class CoAP(DatagramProtocol):
    def __init__(self, multicast=False):
        """
        Initialize the CoAP protocol

        """
        self.received = {}
        self.sent = {}
        self.call_id = {}
        self.relation = {}
        self.blockwise = {}
        self._currentMID = random.randint(1, 1000)

        # Create the resource Tree
        root = Resource('root', self, visible=False, observable=False, allow_children=True)
        root.path = '/'
        # self.root = trie.trie()
        self.root = Tree()
        self.root["/"] = root

        # Initialize layers
        self.request_layer = RequestLayer(self)
        self.blockwise_layer = BlockwiseLayer(self)
        self.resource_layer = ResourceLayer(self)
        self.message_layer = MessageLayer(self)
        self.observe_layer = ObserveLayer(self)

        # Start a task for purge MIDs
        self.l = task.LoopingCall(self.purge_mids)
        self.l.start(defines.EXCHANGE_LIFETIME)

        self.multicast = multicast

    def startProtocol(self):
        """
        Called after protocol has started listening.
        """

        if self.multicast:
            # Set the TTL>1 so multicast will cross router hops:
            self.transport.setTTL(5)
            # Join a specific multicast group:
            self.transport.joinGroup(defines.ALL_COAP_NODES)
            self.transport.setLoopbackMode(True)

    def stopProtocol(self):
        """
        Stop the purge MIDs task

        """
        self.l.stop()

    def parse_path(self, path):
        m = re.match("([a-zA-Z]{4,5})://([a-zA-Z0-9.]*):([0-9]*)/(\S*)", path)
        if m is None:
            m = re.match("([a-zA-Z]{4,5})://([a-zA-Z0-9.]*)/(\S*)", path)
            if m is None:
                m = re.match("([a-zA-Z]{4,5})://([a-zA-Z0-9.]*)", path)
                if m is None:
                    ip, port, path = self.parse_path_ipv6(path)
                else:
                    ip = m.group(2)
                    port = 5683
                    path = ""
            else:
                ip = m.group(2)
                port = 5683
                path = m.group(3)
        else:
            ip = m.group(2)
            port = int(m.group(3))
            path = m.group(4)

        return ip, port, path

    @staticmethod
    def parse_path_ipv6(path):
        m = re.match("([a-zA-Z]{4,5})://\[([a-fA-F0-9:]*)\]:([0-9]*)/(\S*)", path)
        if m is None:
            m = re.match("([a-zA-Z]{4,5})://\[([a-fA-F0-9:]*)\]/(\S*)", path)
            if m is None:
                m = re.match("([a-zA-Z]{4,5})://\[([a-fA-F0-9:]*)\]", path)
                ip = m.group(2)
                port = 5683
                path = ""
            else:
                ip = m.group(2)
                port = 5683
                path = m.group(3)
        else:
            ip = m.group(2)
            port = int(m.group(3))
            path = m.group(4)

        return ip, port, path

    def send(self, message, host, port):
        """
        Send the message

        :param message: the message to send
        :param host: destination host
        :param port: destination port
        """
        print "Message send to " + host + ":" + str(port)
        print "----------------------------------------"
        print message
        print "----------------------------------------"
        serializer = Serializer()
        message = serializer.serialize(message)
        self.transport.write(message, (host, port))

    def datagramReceived(self, data, addr):
        """
        Handler for received UDP datagram.

        :param data: the UDP datagram
        :param host: source host
        :param port: source port
        """
        try:
            host, port = addr
        except ValueError:
            host, port, tmp1, tmp2 = addr
        log.msg("Datagram received from " + str(host) + ":" + str(port))
        serializer = Serializer()
        message = serializer.deserialize(data, host, port)
        print "Message received from " + host + ":" + str(port)
        print "----------------------------------------"
        print message
        print "----------------------------------------"
        if isinstance(message, Request):
            log.msg("Received request")
            ret = self.request_layer.handle_request(message)
            if isinstance(ret, Request):
                response = self.request_layer.process(ret)
            else:
                response = ret
            self.schedule_retrasmission(message, response, None)
            log.msg("Send Response")
            self.send(response, host, port)
        elif isinstance(message, Response):
            log.err("Received response")
            rst = Message.new_rst(message)
            rst = self.message_layer.matcher_response(rst)
            log.msg("Send RST")
            self.send(rst, host, port)
        elif isinstance(message, tuple):
            message, error = message
            response = Response()
            response.destination = (host, port)
            response.code = defines.responses[error]
            response = self.reliability_response(message, response)
            response = self.message_layer.matcher_response(response)
            log.msg("Send Error")
            self.send(response, host, port)
        elif message is not None:
            # ACK or RST
            log.msg("Received ACK or RST")
            self.message_layer.handle_message(message)

    def purge_mids(self):
        """
        Delete messages which has been stored for more than EXCHANGE_LIFETIME.
        Executed in a thread.

        """
        log.msg("Purge MIDs")
        now = time.time()
        sent_key_to_delete = []
        for key in self.sent.keys():
            message, timestamp = self.sent.get(key)
            if timestamp + defines.EXCHANGE_LIFETIME <= now:
                sent_key_to_delete.append(key)
        received_key_to_delete = []
        for key in self.received.keys():
            message, timestamp = self.received.get(key)
            if timestamp + defines.EXCHANGE_LIFETIME <= now:
                received_key_to_delete.append(key)
        for key in sent_key_to_delete:
            del self.sent[key]
        for key in received_key_to_delete:
            del self.received[key]

    def add_resource(self, path, resource):
        """
        Helper function to add resources to the resource Tree during server initialization.
        :param path: path of the resource to create
        :param resource: the actual resource to create
        :return: True, if successful
        """
        assert isinstance(resource, Resource)
        path = path.strip("/")
        paths = path.split("/")
        actual_path = ""
        i = 0
        for p in paths:
            i += 1
            actual_path += "/" + p
            try:
                res = self.root[actual_path]
            except KeyError:
                res = None
            if res is None:
                if len(paths) != i:
                    return False
                resource.path = actual_path
                self.root[actual_path] = resource
        return True

    @property
    def current_mid(self):
        """
        Get the current MID.

        :return: the current MID used by the server.
        """
        return self._currentMID

    @current_mid.setter
    def current_mid(self, mid):
        """
        Set the current MID.

        :param mid: the MID value
        """
        self._currentMID = int(mid)

    def blockwise_response(self, request, response, resource):
        host, port = request.source
        key = hash(str(host) + str(port) + str(request.token))
        if key in self.blockwise:
            # Handle Blockwise transfer
            return self.blockwise_layer.handle_response(key, response, resource), resource
        if resource is not None and len(resource.payload) > defines.MAX_PAYLOAD \
                and request.code == defines.inv_codes["GET"]:
            self.blockwise_layer.start_block2(request)
            return self.blockwise_layer.handle_response(key, response, resource), resource
        return response, resource

    def notify(self, resource):
        """
        Finds the observers that must be notified about the update of the observed resource
        and invoke the notification procedure in different threads.

        :param resource: the node resource updated
        """
        commands = self._observe_layer.notify(resource)
        if commands is not None:
            threads.callMultipleInThread(commands)

    def notify_deletion(self, resource):
        """
        Finds the observers that must be notified about the delete of the observed resource
        and invoke the notification procedure in different threads.

        :param resource: the node resource deleted
        """
        commands = self._observe_layer.notify_deletion(resource)
        if commands is not None:
            threads.callMultipleInThread(commands)

    def remove_observers(self, node):
        """
        Remove all the observers of a resource and and invoke the notification procedure in different threads.

        :type node: coapthon2.utils.Tree
        :param node: the node which has the deleted resource
        """
        commands = self._observe_layer.remove_observers(node)
        if commands is not None:
            threads.callMultipleInThread(commands)

    def prepare_notification(self, t):
        """
        Create the notification message and sends it from the main Thread.

        :type t: (resource, request, response)
        :param t: the arguments of the notification message
        :return: the notification message
        """
        resource, request, notification = self._observe_layer.prepare_notification(t)
        if notification is not None:
            reactor.callFromThread(self._observe_layer.send_notification, (resource, request, notification))

    def prepare_notification_deletion(self, t):
        """
        Create the notification message for deleted resource and sends it from the main Thread.


        :type t: (resource, request, notification)
        :param t: the arguments of the notification message
        :return: the notification message
        """
        resource, request, notification = self._observe_layer.prepare_notification_deletion(t)
        if notification is not None:
            reactor.callFromThread(self._observe_layer.send_notification, (resource, request, notification))

    def schedule_retrasmission(self, request, response, resource):
        """
        Prepare retrasmission message and schedule it for the future.

        :param request:  the request
        :param response: the response
        :param resource: the resource
        """
        host, port = response.destination
        if response.type == defines.inv_types['CON']:
            future_time = random.uniform(defines.ACK_TIMEOUT, (defines.ACK_TIMEOUT * defines.ACK_RANDOM_FACTOR))
            key = hash(str(host) + str(port) + str(response.mid))
            self.call_id[key] = (reactor.callLater(future_time, self.retransmit,
                                                   (request, response, resource, future_time)), 1)

    def retransmit(self, t):
        """
        Retransmit the message and schedule retransmission for future if MAX_RETRANSMIT limit is not already reached.

        :param t: ((Response, Resource), host, port, future_time) or (Response, host, port, future_time)
        """
        log.msg("Retransmit")
        request, response, resource, future_time = t
        host, port = response.destination

        key = hash(str(host) + str(port) + str(response.mid))
        t = self.call_id.get(key)
        if t is None:
            return
        call_id, retransmit_count = t
        if retransmit_count < defines.MAX_RETRANSMIT and (not response.acknowledged and not response.rejected):
            retransmit_count += 1
            self.sent[key] = (response, time.time())
            self.send(response, host, port)
            future_time *= 2
            self.call_id[key] = (reactor.callLater(future_time, self.retransmit,
                                                   (request, response, resource, future_time)), retransmit_count)
        elif retransmit_count >= defines.MAX_RETRANSMIT and (not response.acknowledged and not response.rejected):
            print "Give up on Message " + str(response.mid)
            print "----------------------------------------"
        elif response.acknowledged:
            response.timeouted = False
            del self.call_id[key]
        else:
            response.timeouted = True
            if resource is not None:
                self._observe_layer.remove_observer(resource, request, response)
            del self.call_id[key]

    @staticmethod
    def send_error(request, response, error):
        """
        Send error messages as NON.

        :param request: the request that has generated the error
        :param response: the response message to be filled with the error
        :param error: the error type
        :return: the response
        """
        response.type = defines.inv_types['NON']
        response.code = defines.responses[error]
        response.token = request.token
        response.mid = request.mid
        return response
Exemple #2
0
class CoAP(object):
    def __init__(self, server_address, multicast=False):
        """
        Initialize the CoAP protocol

        """
        host, port = server_address
        ret = socket.getaddrinfo(host, port)
        family, socktype, proto, canonname, sockaddr = ret[0]

        self.stopped = threading.Event()
        self.stopped.clear()
        self.stopped_mid = threading.Event()
        self.stopped_mid.clear()
        self.stopped_ack = threading.Event()
        self.stopped_ack.clear()
        self.executor = concurrent.futures.ThreadPoolExecutor(max_workers=10)
        self.pending_futures = []
        self.executor_req = concurrent.futures.ThreadPoolExecutor(max_workers=10)
        self.received = {}
        self.sent = {}
        self.call_id = {}
        self.relation = {}
        self.blockwise = {}
        self._currentMID = random.randint(1, 1000)

        # Resource directory
        root = Resource('root', self, visible=False, observable=False, allow_children=True)
        root.path = '/'
        self.root = Tree()
        self.root["/"] = root

        # Initialize layers
        self.request_layer = RequestLayer(self)
        self.blockwise_layer = BlockwiseLayer(self)
        self.resource_layer = ResourceLayer(self)
        self.message_layer = MessageLayer(self)
        self.observe_layer = ObserveLayer(self)

        # Clean MIDs
        self.timer_mid = threading.Timer(defines.EXCHANGE_LIFETIME, self.purge_mids)
        self.timer_mid.start()

        self.server_address = server_address
        self.multicast = multicast

        # IPv4 or IPv6
        if len(sockaddr) == 4:
            self._socket = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
            self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        else:
            self._socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
            self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

        if self.multicast:
            # Set some options to make it multicast-friendly
            try:
                    self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
            except AttributeError:
                    pass  # Some systems don't support SO_REUSEPORT
            self._socket.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_TTL, 20)
            self._socket.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_LOOP, 1)

            # Bind to the port
            self._socket.bind(self.server_address)

            # Set some more multicast options
            interface = socket.gethostbyname(socket.gethostname())
            self._socket.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_IF, socket.inet_aton(interface))
            self._socket.setsockopt(socket.SOL_IP, socket.IP_ADD_MEMBERSHIP, socket.inet_aton(self.server_address)
                                    + socket.inet_aton(interface))
        else:
            self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

            self._socket.bind(self.server_address)

    def send(self, message, host, port):
        """
        Send the message

        :param message: the message to send
        :param host: destination host
        :param port: destination port
        """
        # print "Message send to " + host + ":" + str(port)
        # print "----------------------------------------"
        # print message
        # print "----------------------------------------"
        serializer = Serializer()
        message = serializer.serialize(message)

        self._socket.sendto(message, (host, port))

    def listen(self, timeout=10):
        """
        Listen for incoming messages. Timeout is used to check if the server must be switched off.

        :param timeout: Socket Timeout in seconds
        """
        self._socket.settimeout(float(timeout))
        while not self.stopped.isSet():
            try:
                data, client_address = self._socket.recvfrom(4096)
            except socket.timeout:
                continue
            try:
                future = self.executor_req.submit(self.finish_request, (data, client_address))
                future.add_done_callback(self.done_callback)
                self.pending_futures.append(future)
            except RuntimeError:
                print "Exception with Executor"
        self.stopped_ack.set()
        self._socket.close()

    def close(self):
        """
        Stop the server.

        """
        self.stopped.set()
        self.stopped_mid.set()
        while not self.stopped_ack.isSet():
            pass
        for future in self.pending_futures:
            future.cancel()
        self.executor_req.shutdown(True)
        self.executor_req = None
        self.executor.shutdown(True)
        self.executor = None
        self.timer_mid.cancel()
        self.timer_mid = None
        self._socket.close()

    def done_callback(self, future):
        """
        Callback called at the end of the processing of a request.

        :param future: the future object that collects the results
        """
        try:
            message, host, port = future.result()
            self.send(message, host, port)
        except TypeError:
            pass

    def finish_request(self, args):
        """
        Handler for received UDP datagram.

        :param args: (data, (client_ip, client_port)
        """
        data, client_address = args
        host = client_address[0]
        port = client_address[1]

        # logging.log(logging.INFO, "Datagram received from " + str(host) + ":" + str(port))
        serializer = Serializer()
        message = serializer.deserialize(data, host, port)
        # print "Message received from " + host + ":" + str(port)
        # print "----------------------------------------"
        # print message
        # print "----------------------------------------"
        if isinstance(message, Request):
            # log.msg("Received request")
            ret = self.request_layer.handle_request(message)
            if isinstance(ret, Request):
                response = self.request_layer.process(ret)
            else:
                response = ret
            self.schedule_retrasmission(message, response, None)
            # log.msg("Send Response")
            return response, host, port
        elif isinstance(message, Response):
            # log.err("Received response")
            rst = Message.new_rst(message)
            rst = self.message_layer.matcher_response(rst)
            # log.msg("Send RST")
            return rst, host, port
        elif isinstance(message, tuple):
            message, error = message
            response = Response()
            response.destination = (host, port)
            response.code = defines.responses[error]
            response = self.message_layer.reliability_response(message, response)
            response = self.message_layer.matcher_response(response)
            # log.msg("Send Error")
            return response, host, port
        elif message is not None:
            # ACK or RST
            # log.msg("Received ACK or RST")
            self.message_layer.handle_message(message)
            return None

    def purge_mids(self):
        """
        Delete messages which has been stored for more than EXCHANGE_LIFETIME.
        Executed in a thread.

        """
        # log.msg("Purge MIDs")
        while not self.stopped_mid.isSet():
            time.sleep(defines.EXCHANGE_LIFETIME)
            now = time.time()
            sent_key_to_delete = []
            for key in self.sent.keys():
                message, timestamp = self.sent.get(key)
                if timestamp + defines.EXCHANGE_LIFETIME <= now:
                    sent_key_to_delete.append(key)
            received_key_to_delete = []
            for key in self.received.keys():
                message, timestamp = self.received.get(key)
                if timestamp + defines.EXCHANGE_LIFETIME <= now:
                    received_key_to_delete.append(key)
            for key in sent_key_to_delete:
                del self.sent[key]
            for key in received_key_to_delete:
                del self.received[key]
            for future in self.pending_futures:
                if future.done():
                    self.pending_futures.remove(future)
        print "Exit Purge MIDs"

    def add_resource(self, path, resource):
        """
        Helper function to add resources to the resource directory during server initialization.

        :param path: path of the resource to create
        :param resource: the actual resource to create
        :return: True, if successful
        """
        assert isinstance(resource, Resource)
        path = path.strip("/")
        paths = path.split("/")
        actual_path = ""
        i = 0
        for p in paths:
            i += 1
            actual_path += "/" + p
            try:
                res = self.root[actual_path]
            except KeyError:
                res = None
            if res is None:
                if len(paths) != i:
                    return False
                resource.path = actual_path
                self.root[actual_path] = resource
        return True

    @property
    def current_mid(self):
        """
        Get the current MID.

        :return: the current MID used by the server.
        """
        return self._currentMID

    @current_mid.setter
    def current_mid(self, mid):
        """
        Set the current MID.

        :param mid: the MID value
        """
        self._currentMID = int(mid)

    def blockwise_response(self, request, response, resource):
        """
        Verify if a blockwise response is needed.

        :rtype : (response, resource)
        :param request: the request message
        :param response: the partially filled response message
        :param resource: the resource to be put into the message
        :return: the response after blockwise layer and the resource
        """
        host, port = request.source
        key = hash(str(host) + str(port) + str(request.token))
        if key in self.blockwise:
            # Handle Blockwise transfer
            return self.blockwise_layer.handle_response(key, response, resource), resource
        if resource is not None and len(resource.payload) > defines.MAX_PAYLOAD \
                and request.code == defines.inv_codes["GET"]:
            self.blockwise_layer.start_block2(request)
            return self.blockwise_layer.handle_response(key, response, resource), resource
        return response, resource

    def notify(self, resource):
        """
        Finds the observers that must be notified about the update of the observed resource
        and invoke the notification procedure in different threads.

        :param resource: the resource updated
        """
        commands = self.observe_layer.notify(resource)
        if commands is not None:
            for f, t in commands:
                self.pending_futures.append(self.executor.submit(f, t))

    def notify_deletion(self, resource):
        """
        Finds the observers that must be notified about the delete of the observed resource
        and invoke the notification procedure in different threads.

        :param resource: the resource deleted
        """
        commands = self.observe_layer.notify_deletion(resource)
        if commands is not None:
            for f, t in commands:
                self.pending_futures.append(self.executor.submit(f, t))

    def remove_observers(self, path):
        """
        Remove all the observers of a resource and and invoke the notification procedure in different threads.

        :param path: the path of the deleted resource
        """
        commands = self.observe_layer.remove_observers(path)
        if commands is not None:
            for f, t in commands:
                self.pending_futures.append(self.executor.submit(f, t))

    def prepare_notification(self, t):
        """
        Create the notification message and sends it from the main Thread.

        :type t: (resource, request, response)
        :param t: the arguments of the notification message
        :return: the notification message
        """
        resource, request, notification = self.observe_layer.prepare_notification(t)
        if notification is not None:
            self.pending_futures.append(self.executor.submit(self.observe_layer.send_notification,
                                                             (resource, request, notification)))

    def prepare_notification_deletion(self, t):
        """
        Create the notification message for deleted resource and sends it from the main Thread.


        :type t: (resource, request, notification)
        :param t: the arguments of the notification message
        :return: the notification message
        """
        resource, request, notification = self.observe_layer.prepare_notification_deletion(t)
        if notification is not None:
            self.pending_futures.append(self.executor.submit(self.observe_layer.send_notification,
                                                             (resource, request, notification)))

    def schedule_retrasmission(self, request, response, resource):
        """
        Prepare retrasmission message and schedule it for the future.

        :param request:  the request
        :param response: the response
        :param resource: the resource
        """
        host, port = response.destination
        if response.type == defines.inv_types['CON']:
            future_time = random.uniform(defines.ACK_TIMEOUT, (defines.ACK_TIMEOUT * defines.ACK_RANDOM_FACTOR))
            key = hash(str(host) + str(port) + str(response.mid))
            self.call_id[key] = self.executor.submit(self.retransmit, (request, response, resource, future_time))
            self.pending_futures.append(self.call_id[key])

    def retransmit(self, t):
        """
        Retransmit the message and schedule retransmission for future if MAX_RETRANSMIT limit is not already reached.

        :param t: (Request, Response, Resource, future_time)
        """
        # log.msg("Retransmit")
        request, response, resource, future_time = t
        time.sleep(future_time)
        host, port = response.destination

        key = hash(str(host) + str(port) + str(response.mid))
        t = self.call_id.get(key)
        if t is None:
            return
        call_id, retransmit_count = t
        if retransmit_count < defines.MAX_RETRANSMIT and (not response.acknowledged and not response.rejected):
            retransmit_count += 1
            self.sent[key] = (response, time.time())
            self.send(response, host, port)
            future_time *= 2
            self.call_id[key] = self.executor.submit(self.retransmit, (request, response, resource, future_time))
            self.pending_futures.append(self.call_id[key])
        elif retransmit_count >= defines.MAX_RETRANSMIT and (not response.acknowledged and not response.rejected):
            print "Give up on Message " + str(response.mid)
            print "----------------------------------------"
        elif response.acknowledged:
            response.timeouted = False
            del self.call_id[key]
        else:
            response.timeouted = True
            if resource is not None:
                self.observe_layer.remove_observer(resource, request, response)
            del self.call_id[key]

    def send_error(self, request, response, error):
        """
        Send error messages. If request was CON the error will be carried in ACK otherwise NON.

        :param request: the request that has generated the error
        :param response: the response message to be filled with the error
        :param error: the error type
        :return: the response
        """
        if request.type == defines.inv_types['CON'] and not request.acknowledged:
            return self.send_error_ack(request, response, error)
        response.type = defines.inv_types['NON']
        response.code = defines.responses[error]
        response.token = request.token
        response.mid = self.current_mid
        self.current_mid += 1
        return response

    @staticmethod
    def send_error_ack(request, response, error):
        """
        Send error messages as CON.

        :param request: the request that has generated the error
        :param response: the response message to be filled with the error
        :param error: the error type
        :return: the response
        """
        response.type = defines.inv_types['ACK']
        response.code = defines.responses[error]
        response.token = request.token
        response.mid = request.mid
        return response