From 4db21cd7e764c15d1e941545291b6a9989a1c618 Mon Sep 17 00:00:00 2001 From: "giacomo.tanganelli@for.unipi.it" Date: Tue, 23 Jan 2018 11:31:46 +0100 Subject: [PATCH] Porting to Python3 --- .coveragerc | 19 + .travis.yml | 19 + cache_test.py | 493 ++++++ coapclient.py | 162 ++ coapforwardproxy.py | 48 + coapping.py | 107 ++ coapreverseproxy.py | 52 + coapserver.py | 67 + coapthon/__init__.py | 1 + coapthon/caching/__init__.py | 1 + coapthon/caching/cache.py | 246 +++ coapthon/caching/cache.py.bak | 246 +++ coapthon/caching/coapcache.py | 48 + coapthon/caching/coaplrucache.py | 83 ++ coapthon/caching/coaplrucache.py.bak | 83 ++ coapthon/client/__init__.py | 1 + coapthon/client/coap.py | 318 ++++ coapthon/client/coap.py.bak | 317 ++++ coapthon/client/helperclient.py | 236 +++ coapthon/client/helperclient.py.bak | 236 +++ coapthon/defines.py | 300 ++++ coapthon/forward_proxy/__init__.py | 1 + coapthon/forward_proxy/coap.py | 366 +++++ coapthon/http_proxy/__init__.py | 1 + coapthon/http_proxy/http_coap_proxy.py | 283 ++++ coapthon/http_proxy/http_coap_proxy.py.bak | 283 ++++ coapthon/layers/__init__.py | 1 + coapthon/layers/blocklayer.py | 309 ++++ coapthon/layers/cachelayer.py | 111 ++ coapthon/layers/forwardLayer.py | 167 +++ coapthon/layers/messagelayer.py | 318 ++++ coapthon/layers/messagelayer.py.bak | 314 ++++ coapthon/layers/observelayer.py | 199 +++ coapthon/layers/observelayer.py.bak | 199 +++ coapthon/layers/requestlayer.py | 142 ++ coapthon/layers/resourcelayer.py | 564 +++++++ coapthon/messages/__init__.py | 1 + coapthon/messages/message.py | 695 +++++++++ coapthon/messages/message.py.bak | 693 +++++++++ coapthon/messages/option.py | 137 ++ coapthon/messages/request.py | 260 ++++ coapthon/messages/request.py.bak | 260 ++++ coapthon/messages/response.py | 124 ++ coapthon/resources/__init__.py | 1 + coapthon/resources/remoteResource.py | 11 + coapthon/resources/resource.py | 516 +++++++ coapthon/resources/resource.py.bak | 514 +++++++ coapthon/reverse_proxy/__init__.py | 1 + coapthon/reverse_proxy/coap.py | 477 ++++++ coapthon/serializer.py | 408 +++++ coapthon/serializer.py.bak | 397 +++++ coapthon/server/__init__.py | 1 + coapthon/server/coap.py | 422 ++++++ coapthon/server/coap.py.bak | 421 ++++++ coapthon/transaction.py | 156 ++ coapthon/utils.py | 188 +++ coapthon/utils.py.bak | 188 +++ collectclient.py | 141 ++ collectserver.py | 337 +++++ coverage_test.py | 1576 ++++++++++++++++++++ coverage_testIPv6.py | 164 ++ coverage_test_advanced.py | 214 +++ coverage_test_multicast.py | 165 ++ coverage_test_multicast_ipv6.py | 165 ++ coverage_test_proxy.py | 1421 ++++++++++++++++++ coverage_test_reverse_proxy.py | 1473 ++++++++++++++++++ dimmerserver.py | 74 + exampleresources.py | 281 ++++ logging.conf | 22 + lorem_impsum.txt | 7 + plugtest.py | 889 +++++++++++ plugtest_coapserver.py | 75 + plugtest_resources.py | 150 ++ requirements.txt | 3 + reverse_proxy_mapping.xml | 4 + setup.py | 15 + 76 files changed, 19388 insertions(+) create mode 100644 .coveragerc create mode 100644 .travis.yml create mode 100644 cache_test.py create mode 100644 coapclient.py create mode 100644 coapforwardproxy.py create mode 100644 coapping.py create mode 100644 coapreverseproxy.py create mode 100644 coapserver.py create mode 100644 coapthon/__init__.py create mode 100644 coapthon/caching/__init__.py create mode 100644 coapthon/caching/cache.py create mode 100644 coapthon/caching/cache.py.bak create mode 100644 coapthon/caching/coapcache.py create mode 100644 coapthon/caching/coaplrucache.py create mode 100644 coapthon/caching/coaplrucache.py.bak create mode 100644 coapthon/client/__init__.py create mode 100644 coapthon/client/coap.py create mode 100644 coapthon/client/coap.py.bak create mode 100644 coapthon/client/helperclient.py create mode 100644 coapthon/client/helperclient.py.bak create mode 100644 coapthon/defines.py create mode 100644 coapthon/forward_proxy/__init__.py create mode 100644 coapthon/forward_proxy/coap.py create mode 100644 coapthon/http_proxy/__init__.py create mode 100644 coapthon/http_proxy/http_coap_proxy.py create mode 100644 coapthon/http_proxy/http_coap_proxy.py.bak create mode 100644 coapthon/layers/__init__.py create mode 100644 coapthon/layers/blocklayer.py create mode 100644 coapthon/layers/cachelayer.py create mode 100644 coapthon/layers/forwardLayer.py create mode 100644 coapthon/layers/messagelayer.py create mode 100644 coapthon/layers/messagelayer.py.bak create mode 100644 coapthon/layers/observelayer.py create mode 100644 coapthon/layers/observelayer.py.bak create mode 100644 coapthon/layers/requestlayer.py create mode 100644 coapthon/layers/resourcelayer.py create mode 100644 coapthon/messages/__init__.py create mode 100644 coapthon/messages/message.py create mode 100644 coapthon/messages/message.py.bak create mode 100644 coapthon/messages/option.py create mode 100644 coapthon/messages/request.py create mode 100644 coapthon/messages/request.py.bak create mode 100644 coapthon/messages/response.py create mode 100644 coapthon/resources/__init__.py create mode 100644 coapthon/resources/remoteResource.py create mode 100644 coapthon/resources/resource.py create mode 100644 coapthon/resources/resource.py.bak create mode 100644 coapthon/reverse_proxy/__init__.py create mode 100644 coapthon/reverse_proxy/coap.py create mode 100644 coapthon/serializer.py create mode 100644 coapthon/serializer.py.bak create mode 100644 coapthon/server/__init__.py create mode 100644 coapthon/server/coap.py create mode 100644 coapthon/server/coap.py.bak create mode 100644 coapthon/transaction.py create mode 100644 coapthon/utils.py create mode 100644 coapthon/utils.py.bak create mode 100644 collectclient.py create mode 100644 collectserver.py create mode 100644 coverage_test.py create mode 100644 coverage_testIPv6.py create mode 100644 coverage_test_advanced.py create mode 100644 coverage_test_multicast.py create mode 100644 coverage_test_multicast_ipv6.py create mode 100644 coverage_test_proxy.py create mode 100644 coverage_test_reverse_proxy.py create mode 100644 dimmerserver.py create mode 100644 exampleresources.py create mode 100644 logging.conf create mode 100644 lorem_impsum.txt create mode 100644 plugtest.py create mode 100644 plugtest_coapserver.py create mode 100644 plugtest_resources.py create mode 100644 requirements.txt create mode 100644 reverse_proxy_mapping.xml create mode 100644 setup.py diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..4e63340 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,19 @@ +[report] +# Regexes for lines to exclude from consideration +exclude_lines = + # Have to re-enable the standard pragma + pragma: no cover + + # Don't complain if tests don't hit defensive assertion code: + raise AssertionError + except RuntimeError + except ValueError + raise KeyError + except IndexError + except AttributeError + except KeyError + raise NotImplementedError + + # Don't complain if non-runnable code isn't run: + if 0: + if __name__ == .__main__.: diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..172114c --- /dev/null +++ b/.travis.yml @@ -0,0 +1,19 @@ +language: python +python: +- "3.5" +# command to install dependencies +install: +- pip install -r requirements.txt +- pip install coveralls + +script: +- coverage run plugtest.py +- coverage run -a coverage_test.py +- coverage run -a coverage_test_proxy.py +- coverage run -a coverage_test_reverse_proxy.py +#- coverage run -a coverage_testIPv6.py +- coverage run -a coverage_test_advanced.py +- coverage run -a coverage_test_multicast.py +- coverage run -a cache_test.py +after_success: +- coveralls \ No newline at end of file diff --git a/cache_test.py b/cache_test.py new file mode 100644 index 0000000..42d8cf5 --- /dev/null +++ b/cache_test.py @@ -0,0 +1,493 @@ +from queue import Queue +import random +import socket +import threading +import unittest +from coapclient import HelperClient +from coapforwardproxy import CoAPForwardProxy +from coapserver import CoAPServer +from coapthon import defines +from coapthon.messages.option import Option +from coapthon.messages.request import Request +from coapthon.messages.response import Response +from coapthon.serializer import Serializer +import time + +__author__ = 'Emilio Vallati' +__version__ = "1.0" + + +class Tests(unittest.TestCase): + + def setUp(self): + self.server_address = ("127.0.0.1", 5683) + self.current_mid = random.randint(1, 1000) + self.server_mid = random.randint(1000, 2000) + self.server = CoAPServer("127.0.0.1", 5684) + self.server_thread = threading.Thread(target=self.server.listen, args=(10,)) + self.server_thread.start() + self.proxy = CoAPForwardProxy("127.0.0.1", 5683, cache=True) + self.proxy_thread = threading.Thread(target=self.proxy.listen, args=(10,)) + self.proxy_thread.start() + self.queue = Queue() + + def tearDown(self): + self.server.close() + self.server_thread.join(timeout=25) + self.server = None + self.proxy.close() + self.proxy_thread.join(timeout=25) + self.proxy = None + + def _test_with_client_delayed(self, message_list): # pragma: no cover + client = HelperClient(self.server_address) + for message, expected in message_list: + if message is not None: + received_message = client.send_request(message) + time.sleep(5) + + if expected is not None: + if expected.etag is not None: + self.assertEqual(received_message.etag, expected.etag) + if expected.type is not None: + self.assertEqual(received_message.type, expected.type) + if expected.mid is not None: + self.assertEqual(received_message.mid, expected.mid) + self.assertEqual(received_message.code, expected.code) + if expected.source is not None: + self.assertEqual(received_message.source, self.server_address) + if expected.token is not None: + self.assertEqual(received_message.token, expected.token) + if expected.payload is not None: + self.assertEqual(received_message.payload, expected.payload) + if expected.max_age is not None: + if expected.max_age != 60: + self.assertNotEqual(received_message.max_age, 60) + else: + self.assertEqual(received_message.max_age, expected.max_age) + if expected.options: + self.assertEqual(len(received_message.options), len(expected.options)) + for o in expected.options: + assert isinstance(o, Option) + if o.name != defines.OptionRegistry.MAX_AGE.name and o.name != defines.OptionRegistry.ETAG.name: + option_value = getattr(expected, o.name.lower().replace("-", "_")) + option_value_rec = getattr(received_message, o.name.lower().replace("-", "_")) + self.assertEqual(option_value, option_value_rec) + client.stop() + + def client_callback(self, response): + print("Callback") + self.queue.put(response) + + def test_get_multiple(self): + print("TEST_GET_MULTIPLE") + path = "/basic" + req = Request() + req.code = defines.Codes.GET.number + req.uri_path = path + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + req.proxy_uri = "coap://127.0.0.1:5684/basic" + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = self.current_mid + expected.code = defines.Codes.CONTENT.number + expected.token = None + expected.payload = "Basic Resource" + + exchange1 = (req, expected) + + self.current_mid += 1 + + # PREPARING SECOND EXPECTED RESPONSE (MAX AGE MUST BE CHECKED) + req2 = Request() + req2.code = defines.Codes.GET.number + req2.uri_path = path + req2.type = defines.Types["CON"] + req2._mid = self.current_mid + req2.destination = self.server_address + req2.proxy_uri = "coap://127.0.0.1:5684/basic" + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = self.current_mid + expected.code = defines.Codes.CONTENT.number + expected.token = None + expected.payload = "Basic Resource" + expected.max_age = 61 + + exchange2 = (req2, expected) + + self._test_with_client_delayed([exchange1, exchange2]) + + def test_get_post(self): + print("TEST_GET_POST") + path = "/basic" + req = Request() + req.code = defines.Codes.POST.number + req.uri_path = path + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + req.proxy_uri = "coap://127.0.0.1:5684/storage/new" + req.payload = "Hello" + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = self.current_mid + expected.code = defines.Codes.CREATED.number + expected.token = None + expected.payload = None + + exchange1 = (req, expected) + + self.current_mid += 1 + + # PREPARING SECOND EXPECTED RESPONSE + req2 = Request() + req2.code = defines.Codes.GET.number + req2.uri_path = path + req2.type = defines.Types["CON"] + req2._mid = self.current_mid + req2.destination = self.server_address + req2.proxy_uri = "coap://127.0.0.1:5684/storage/new" + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = self.current_mid + expected.code = defines.Codes.CONTENT.number + expected.token = None + expected.payload = "Hello" + + exchange2 = (req2, expected) + + self.current_mid += 1 + + # PREPARING THIRD EXPECTED RESPONSE + req3 = Request() + req3.code = defines.Codes.POST.number + req3.uri_path = path + req3.type = defines.Types["CON"] + req3._mid = self.current_mid + req3.destination = self.server_address + req3.proxy_uri = "coap://127.0.0.1:5684/storage/new" + req3.payload = "Hello" + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = self.current_mid + expected.code = defines.Codes.CREATED.number + expected.token = None + expected.payload = None + + exchange3 = (req3, expected) + + self.current_mid += 1 + + # PREPARING FOURTH EXPECTED RESPONSE + req4 = Request() + req4.code = defines.Codes.GET.number + req4.uri_path = path + req4.type = defines.Types["CON"] + req4._mid = self.current_mid + req4.destination = self.server_address + req4.proxy_uri = "coap://127.0.0.1:5684/storage/new" + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = self.current_mid + expected.code = defines.Codes.CONTENT.number + expected.token = None + expected.payload = "Hello" + + exchange4 = (req4, expected) + + self.current_mid += 1 + + self._test_with_client_delayed([exchange1, exchange2, exchange3, exchange4]) + + def test_get_put(self): + print("TEST_GET_PUT") + path = "/basic" + req = Request() + req.code = defines.Codes.POST.number + req.uri_path = path + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + req.proxy_uri = "coap://127.0.0.1:5684/storage/new" + req.payload = "Hello" + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = self.current_mid + expected.code = defines.Codes.CREATED.number + expected.token = None + expected.payload = None + + exchange1 = (req, expected) + + self.current_mid += 1 + + # PREPARING SECOND EXPECTED RESPONSE + req2 = Request() + req2.code = defines.Codes.GET.number + req2.uri_path = path + req2.type = defines.Types["CON"] + req2._mid = self.current_mid + req2.destination = self.server_address + req2.proxy_uri = "coap://127.0.0.1:5684/storage/new" + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = self.current_mid + expected.code = defines.Codes.CONTENT.number + expected.token = None + expected.payload = "Hello" + + exchange2 = (req2, expected) + + self.current_mid += 1 + + # PREPARING THIRD EXPECTED RESPONSE + req3 = Request() + req3.code = defines.Codes.PUT.number + req3.uri_path = path + req3.type = defines.Types["CON"] + req3._mid = self.current_mid + req3.destination = self.server_address + req3.proxy_uri = "coap://127.0.0.1:5684/storage/new" + req3.payload = "Hello" + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = self.current_mid + expected.code = defines.Codes.CHANGED.number + expected.token = None + expected.payload = None + + exchange3 = (req3, expected) + + self.current_mid += 1 + + # PREPARING FOURTH EXPECTED RESPONSE + req4 = Request() + req4.code = defines.Codes.GET.number + req4.uri_path = path + req4.type = defines.Types["CON"] + req4._mid = self.current_mid + req4.destination = self.server_address + req4.proxy_uri = "coap://127.0.0.1:5684/storage/new" + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = self.current_mid + expected.code = defines.Codes.CONTENT.number + expected.token = None + expected.payload = "Hello" + + exchange4 = (req4, expected) + + self.current_mid += 1 + + self._test_with_client_delayed([exchange1, exchange2, exchange3, exchange4]) + + def test_get_delete(self): + print("TEST_GET_DELETE") + path = "/basic" + + req2 = Request() + req2.code = defines.Codes.GET.number + req2.uri_path = path + req2.type = defines.Types["CON"] + req2._mid = self.current_mid + req2.destination = self.server_address + req2.proxy_uri = "coap://127.0.0.1:5684/storage/new" + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = self.current_mid + expected.code = defines.Codes.NOT_FOUND.number + expected.token = None + expected.payload = None + + exchange0 = (req2, expected) + + self.current_mid += 1 + + req = Request() + req.code = defines.Codes.POST.number + req.uri_path = path + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + req.proxy_uri = "coap://127.0.0.1:5684/storage/new" + req.payload = "Hello" + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = self.current_mid + expected.code = defines.Codes.CREATED.number + expected.token = None + expected.payload = None + + exchange1 = (req, expected) + + self.current_mid += 1 + + # PREPARING SECOND EXPECTED RESPONSE + req2 = Request() + req2.code = defines.Codes.GET.number + req2.uri_path = path + req2.type = defines.Types["CON"] + req2._mid = self.current_mid + req2.destination = self.server_address + req2.proxy_uri = "coap://127.0.0.1:5684/storage/new" + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = self.current_mid + expected.code = defines.Codes.CONTENT.number + expected.token = None + expected.payload = "Hello" + + exchange2 = (req2, expected) + + self.current_mid += 1 + + # PREPARING THIRD EXPECTED RESPONSE + req3 = Request() + req3.code = defines.Codes.DELETE.number + req3.uri_path = path + req3.type = defines.Types["CON"] + req3._mid = self.current_mid + req3.destination = self.server_address + req3.proxy_uri = "coap://127.0.0.1:5684/storage/new" + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = self.current_mid + expected.code = defines.Codes.DELETED.number + expected.token = None + expected.payload = None + + exchange3 = (req3, expected) + + self.current_mid += 1 + + # PREPARING FOURTH EXPECTED RESPONSE + req4 = Request() + req4.code = defines.Codes.GET.number + req4.uri_path = path + req4.type = defines.Types["CON"] + req4._mid = self.current_mid + req4.destination = self.server_address + req4.proxy_uri = "coap://127.0.0.1:5684/storage/new" + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = self.current_mid + expected.code = defines.Codes.NOT_FOUND.number + expected.token = None + + exchange4 = (req4, expected) + + self.current_mid += 1 + + self._test_with_client_delayed([exchange0, exchange1, exchange2, exchange3, exchange4]) + + def test_get_etag(self): + print("TEST_GET_ETAG") + path = "/etag" + req = Request() + req.code = defines.Codes.GET.number + req.uri_path = path + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + req.proxy_uri = "coap://127.0.0.1:5684/etag" + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = self.current_mid + expected.code = defines.Codes.CONTENT.number + expected.token = None + expected.payload = None + expected.etag = str(0) + + exchange1 = (req, expected) + + self.current_mid += 1 + + # PREPARING SECOND EXPECTED RESPONSE + req2 = Request() + req2.code = defines.Codes.GET.number + req2.uri_path = path + req2.type = defines.Types["CON"] + req2._mid = self.current_mid + req2.destination = self.server_address + req2.proxy_uri = "coap://127.0.0.1:5684/etag" + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = self.current_mid + expected.code = defines.Codes.CONTENT.number + expected.token = None + expected.etag = str(0) + expected.max_age = 1 + + exchange2 = (req2, expected) + + self.current_mid += 1 + + # PREPARING THIRD EXPECTED RESPONSE + req3 = Request() + req3.code = defines.Codes.POST.number + req3.uri_path = path + req3.type = defines.Types["CON"] + req3._mid = self.current_mid + req3.destination = self.server_address + req3.proxy_uri = "coap://127.0.0.1:5684/etag" + req3.payload = "Hello" + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = self.current_mid + expected.code = defines.Codes.CHANGED.number + expected.token = None + expected.payload = None + expected.etag = str(1) + expected.location_path = "etag" + + exchange3 = (req3, expected) + + self.current_mid += 1 + + # PREPARING FOURTH EXPECTED RESPONSE + req4 = Request() + req4.code = defines.Codes.GET.number + req4.uri_path = path + req4.type = defines.Types["CON"] + req4._mid = self.current_mid + req4.destination = self.server_address + req4.proxy_uri = "coap://127.0.0.1:5684/etag" + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = self.current_mid + expected.code = defines.Codes.CONTENT.number + expected.token = None + expected.payload = "Hello" + expected.etag = str(1) + + exchange4 = (req4, expected) + + self.current_mid += 1 + + self._test_with_client_delayed([exchange1, exchange2, exchange3, exchange4]) + + +if __name__ == '__main__': + unittest.main() diff --git a/coapclient.py b/coapclient.py new file mode 100644 index 0000000..e4d4d9a --- /dev/null +++ b/coapclient.py @@ -0,0 +1,162 @@ +#!/usr/bin/env python +import getopt +import socket +import sys + +from coapthon.client.helperclient import HelperClient +from coapthon.utils import parse_uri + +__author__ = 'Giacomo Tanganelli' + +client = None + + +def usage(): # pragma: no cover + print("Command:\tcoapclient.py -o -p [-P]") + print("Options:") + print("\t-o, --operation=\tGET|PUT|POST|DELETE|DISCOVER|OBSERVE") + print("\t-p, --path=\t\t\tPath of the request") + print("\t-P, --payload=\t\tPayload of the request") + print("\t-f, --payload-file=\t\tFile with payload of the request") + + +def client_callback(response): + print("Callback") + + +def client_callback_observe(response): # pragma: no cover + global client + print("Callback_observe") + check = True + while check: + chosen = eval(input("Stop observing? [y/N]: ")) + if chosen != "" and not (chosen == "n" or chosen == "N" or chosen == "y" or chosen == "Y"): + print("Unrecognized choose.") + continue + elif chosen == "y" or chosen == "Y": + while True: + rst = eval(input("Send RST message? [Y/n]: ")) + if rst != "" and not (rst == "n" or rst == "N" or rst == "y" or rst == "Y"): + print("Unrecognized choose.") + continue + elif rst == "" or rst == "y" or rst == "Y": + client.cancel_observing(response, True) + else: + client.cancel_observing(response, False) + check = False + break + else: + break + + +def main(): # pragma: no cover + global client + op = None + path = None + payload = None + try: + opts, args = getopt.getopt(sys.argv[1:], "ho:p:P:f:", ["help", "operation=", "path=", "payload=", + "payload_file="]) + except getopt.GetoptError as err: + # print help information and exit: + print((str(err))) # will print something like "option -a not recognized" + usage() + sys.exit(2) + for o, a in opts: + if o in ("-o", "--operation"): + op = a + elif o in ("-p", "--path"): + path = a + elif o in ("-P", "--payload"): + payload = a + elif o in ("-f", "--payload-file"): + with open(a, 'r') as f: + payload = f.read() + elif o in ("-h", "--help"): + usage() + sys.exit() + else: + usage() + sys.exit(2) + + if op is None: + print("Operation must be specified") + usage() + sys.exit(2) + + if path is None: + print("Path must be specified") + usage() + sys.exit(2) + + if not path.startswith("coap://"): + print("Path must be conform to coap://host[:port]/path") + usage() + sys.exit(2) + + host, port, path = parse_uri(path) + try: + tmp = socket.gethostbyname(host) + host = tmp + except socket.gaierror: + pass + client = HelperClient(server=(host, port)) + if op == "GET": + if path is None: + print("Path cannot be empty for a GET request") + usage() + sys.exit(2) + response = client.get(path) + print((response.pretty_print())) + client.stop() + elif op == "OBSERVE": + if path is None: + print("Path cannot be empty for a GET request") + usage() + sys.exit(2) + client.observe(path, client_callback_observe) + + elif op == "DELETE": + if path is None: + print("Path cannot be empty for a DELETE request") + usage() + sys.exit(2) + response = client.delete(path) + print((response.pretty_print())) + client.stop() + elif op == "POST": + if path is None: + print("Path cannot be empty for a POST request") + usage() + sys.exit(2) + if payload is None: + print("Payload cannot be empty for a POST request") + usage() + sys.exit(2) + response = client.post(path, payload) + print((response.pretty_print())) + client.stop() + elif op == "PUT": + if path is None: + print("Path cannot be empty for a PUT request") + usage() + sys.exit(2) + if payload is None: + print("Payload cannot be empty for a PUT request") + usage() + sys.exit(2) + response = client.put(path, payload) + print((response.pretty_print())) + client.stop() + elif op == "DISCOVER": + response = client.discover() + print((response.pretty_print())) + client.stop() + else: + print("Operation not recognized") + usage() + sys.exit(2) + + +if __name__ == '__main__': # pragma: no cover + main() diff --git a/coapforwardproxy.py b/coapforwardproxy.py new file mode 100644 index 0000000..9a6e1d5 --- /dev/null +++ b/coapforwardproxy.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python + +import getopt +import sys +from coapthon.forward_proxy.coap import CoAP + +__author__ = 'Giacomo Tanganelli' + + +class CoAPForwardProxy(CoAP): + def __init__(self, host, port, multicast=False, cache=False): + CoAP.__init__(self, (host, port), multicast=multicast, cache=cache) + + print(("CoAP Proxy start on " + host + ":" + str(port))) + + +def usage(): # pragma: no cover + print("coapforwardproxy.py -i -p ") + + +def main(argv): # pragma: no cover + ip = "0.0.0.0" + port = 5684 + try: + opts, args = getopt.getopt(argv, "hi:p:", ["ip=", "port="]) + except getopt.GetoptError: + usage() + sys.exit(2) + for opt, arg in opts: + if opt == '-h': + usage() + sys.exit() + elif opt in ("-i", "--ip"): + ip = arg + elif opt in ("-p", "--port"): + port = int(arg) + + server = CoAPForwardProxy(ip, port) + try: + server.listen(10) + except KeyboardInterrupt: + print("Server Shutdown") + server.close() + print("Exiting...") + + +if __name__ == "__main__": # pragma: no cover + main(sys.argv[1:]) diff --git a/coapping.py b/coapping.py new file mode 100644 index 0000000..8ba6787 --- /dev/null +++ b/coapping.py @@ -0,0 +1,107 @@ +#!/usr/bin/env python + +# COAP ping implementation +# 0x4000 0001 <--> 0x7000 0001 +# 0x4000 0002 <--> 0x7000 0002 +# 0x4000 0003 <--> 0x7000 0003 + +import socket +import struct +import sys +from time import sleep, time +from optparse import OptionParser + + +# Parse Options +if __name__ == '__main__': + parser = OptionParser() + + parser.add_option("-n", "--hostname", + action="append", + dest="host_name", + help="Define COAP host name") + + parser.add_option("-p", "--port", + action="append", + dest="host_port", + default=5683, + help="Define COAP host port (default: 5683)") + + parser.add_option("-l", "--loops", + type="int", + dest="ping_loops", + default=0, + help="Number of ping loops (default: 0 - forever)") + + parser.add_option("-t", "--sleep", + type="float", + dest="sleep_sec", + default=1, + help="Time in seconds between two pings (default: 1 sec)") + + (options, args) = parser.parse_args() + + # COAP ping parameters setup + host = options.host_name[0] + port = options.host_port + sleep_sec = options.sleep_sec + ping_loops = options.ping_loops + + ping_no = 1 # ping payload counter + ping_cnt = 0 # global ping cnt + + print('COAP ping script') + print(('COAP ping to: %s:%s...' % (host, port))) + + try: + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + except socket.error: + print('Error: Failed to create socket') + sys.exit() + + while(1): + loop_time = time() + msg = '' #[0x40, 0x00, 0x00, 0x00] + + msg += struct.pack("B", 0x40) + msg += struct.pack("B", 0x00) + msg += struct.pack("B", 0x00) + msg += struct.pack("B", ping_no) + + try : + print(('[0x%08X] Send ping:' % (ping_cnt), [hex(ord(c)) for c in msg])) + #Set the whole string + s.sendto(msg, (host, port)) + s.settimeout(2 + sleep_sec) + + # receive data from client (data, addr) + d = s.recvfrom(4) + reply = d[0] + addr = d[1] + + # We need to check if ping peyload counter is the same in reply + status = bytes(msg)[3] == bytes(reply)[3] + print(('[0x%08X] Recv ping:' % (ping_cnt), [hex(ord(c)) for c in reply], 'ok' if status else 'fail')) + + except socket.error as e: + print(('Error: socket.error: ', str(e))) + #print 'Error Code : ' + str(msg[0]) + ' Message ' + msg[1] + sleep(3) # Waiting to recover ;) + except socket.timeout: + print("Error: closing socket") + s.close() + + if ping_no >= 0xFF: + ping_no = 1 + else: + ping_no += 1 + + sleep(sleep_sec - (time() - loop_time)) + print(('In %.2f sec' % (time() - loop_time))) + + if ping_loops > 0: + if ping_loops == 1: + break + ping_loops -= 1 + + ping_cnt += 1 diff --git a/coapreverseproxy.py b/coapreverseproxy.py new file mode 100644 index 0000000..b93fb44 --- /dev/null +++ b/coapreverseproxy.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python + +import getopt +import sys +from coapthon.reverse_proxy.coap import CoAP + +__author__ = 'Giacomo Tanganelli' + + +class CoAPReverseProxy(CoAP): + def __init__(self, host, port, xml_file, multicast=False, cache=False, starting_mid=None): + CoAP.__init__(self, (host, port), xml_file=xml_file, multicast=multicast, starting_mid=starting_mid, + cache=cache) + + print(("CoAP Proxy start on " + host + ":" + str(port))) + + +def usage(): # pragma: no cover + print("coapreverseproxy.py -i -p -f ") + + +def main(argv): # pragma: no cover + ip = "0.0.0.0" + port = 5684 + file_xml = "reverse_proxy_mapping.xml" + try: + opts, args = getopt.getopt(argv, "hi:p:f:", ["ip=", "port=", "file="]) + except getopt.GetoptError: + usage() + sys.exit(2) + for opt, arg in opts: + if opt == '-h': + usage() + sys.exit() + elif opt in ("-i", "--ip"): + ip = arg + elif opt in ("-p", "--port"): + port = int(arg) + elif opt in ("-f", "--file"): + file_xml = arg + + server = CoAPReverseProxy(ip, port, file_xml) + try: + server.listen(10) + except KeyboardInterrupt: + print("Server Shutdown") + server.close() + print("Exiting...") + + +if __name__ == "__main__": # pragma: no cover + main(sys.argv[1:]) diff --git a/coapserver.py b/coapserver.py new file mode 100644 index 0000000..aef8b9c --- /dev/null +++ b/coapserver.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python + +import getopt +import sys +from coapthon.server.coap import CoAP +from exampleresources import BasicResource, Long, Separate, Storage, Big, voidResource, XMLResource, ETAGResource, \ + Child, \ + MultipleEncodingResource, AdvancedResource, AdvancedResourceSeparate + +__author__ = 'Giacomo Tanganelli' + + +class CoAPServer(CoAP): + def __init__(self, host, port, multicast=False): + CoAP.__init__(self, (host, port), multicast) + self.add_resource('basic/', BasicResource()) + self.add_resource('storage/', Storage()) + self.add_resource('separate/', Separate()) + self.add_resource('long/', Long()) + self.add_resource('big/', Big()) + self.add_resource('void/', voidResource()) + self.add_resource('xml/', XMLResource()) + self.add_resource('encoding/', MultipleEncodingResource()) + self.add_resource('etag/', ETAGResource()) + self.add_resource('child/', Child()) + self.add_resource('advanced/', AdvancedResource()) + self.add_resource('advancedSeparate/', AdvancedResourceSeparate()) + + print(("CoAP Server start on " + host + ":" + str(port))) + print((self.root.dump())) + + +def usage(): # pragma: no cover + print("coapserver.py -i -p ") + + +def main(argv): # pragma: no cover + ip = "0.0.0.0" + port = 5683 + multicast = False + try: + opts, args = getopt.getopt(argv, "hi:p:m", ["ip=", "port=", "multicast"]) + except getopt.GetoptError: + usage() + sys.exit(2) + for opt, arg in opts: + if opt == '-h': + usage() + sys.exit() + elif opt in ("-i", "--ip"): + ip = arg + elif opt in ("-p", "--port"): + port = int(arg) + elif opt in ("-m", "--multicast"): + multicast = True + + server = CoAPServer(ip, port, multicast) + try: + server.listen(10) + except KeyboardInterrupt: + print("Server Shutdown") + server.close() + print("Exiting...") + + +if __name__ == "__main__": # pragma: no cover + main(sys.argv[1:]) diff --git a/coapthon/__init__.py b/coapthon/__init__.py new file mode 100644 index 0000000..0dc55fa --- /dev/null +++ b/coapthon/__init__.py @@ -0,0 +1 @@ +__author__ = 'Giacomo Tanganelli' diff --git a/coapthon/caching/__init__.py b/coapthon/caching/__init__.py new file mode 100644 index 0000000..2c01446 --- /dev/null +++ b/coapthon/caching/__init__.py @@ -0,0 +1 @@ +author = "Emilio" \ No newline at end of file diff --git a/coapthon/caching/cache.py b/coapthon/caching/cache.py new file mode 100644 index 0000000..c87a408 --- /dev/null +++ b/coapthon/caching/cache.py @@ -0,0 +1,246 @@ +import time + +import logging + +from .coaplrucache import CoapLRUCache +from coapthon import utils +from coapthon.messages.request import * + +__author__ = 'Emilio Vallati' + +logger = logging.getLogger(__name__) + + +class Cache(object): + + def __init__(self, mode, max_dim): + """ + + :param max_dim: max number of elements in the cache + :param mode: used to differentiate between a cache used in a forward-proxy or in a reverse-proxy + """ + + self.max_dimension = max_dim + self.mode = mode + self.cache = CoapLRUCache(max_dim) + + def cache_add(self, request, response): + """ + checks for full cache and valid code before updating the cache + + :param request: + :param response: + :return: + """ + logger.debug("adding response to the cache") + + """ + checking for valid code +- """ + code = response.code + try: + utils.check_code(code) + except utils.InvalidResponseCode: # pragma no cover + logger.error("Invalid response code") + return + + """ + return if max_age is 0 + """ + if response.max_age == 0: + return + + """ + Initialising new cache element based on the mode and updating the cache + """ + + if self.mode == defines.FORWARD_PROXY: + new_key = CacheKey(request) + else: + new_key = ReverseCacheKey(request) + + logger.debug("MaxAge = {maxage}".format(maxage=response.max_age)) + new_element = CacheElement(new_key, response, request, response.max_age) + + self.cache.update(new_key, new_element) + logger.debug("Cache Size = {size}".format(size=self.cache.debug_print())) + + def search_related(self, request): + logger.debug("Cache Search Request") + if self.cache.is_empty() is True: + logger.debug("Empty Cache") + return None + + """ + extracting everything from the cache + """ + result = [] + items = list(self.cache.cache.items()) + + for key, item in items: + element = self.cache.get(item.key) + logger.debug("Element : {elm}".format(elm=str(element))) + + if request.proxy_uri == element.uri: + result.append(item) + + return result + + def search_response(self, request): + """ + creates a key from the request and searches the cache with it + + :param request: + :return CacheElement: returns None if there's a cache miss + """ + logger.debug("Cache Search Response") + + if self.cache.is_empty() is True: + logger.debug("Empty Cache") + return None + + """ + create a new cache key from the request + """ + + if self.mode == defines.FORWARD_PROXY: + search_key = CacheKey(request) + else: + search_key = ReverseCacheKey(request) + + response = self.cache.get(search_key) + + return response + + def validate(self, request, response): + """ + refreshes a resource when a validation response is received + + :param request: + :param response: + :return: + """ + element = self.search_response(request) + if element is not None: + element.cached_response.options = response.options + element.freshness = True + element.max_age = response.max_age + element.creation_time = time.time() + element.uri = request.proxy_uri + + def mark(self, element): + """ + marks the requested resource in the cache as not fresh + :param element: + :return: + """ + logger.debug("Element : {elm}".format(elm=str(element))) + if element is not None: + logger.debug("Mark as not fresh") + element.freshness = False + logger.debug(str(self.cache)) + +""" +class for the element contained in the cache +""" + + +class CacheElement(object): + def __init__(self, cache_key, response, request, max_age=60): + """ + + :param cache_key: the key used to search for the element in the cache + :param response: the server response to store + :param max_age: maximum number of seconds that the resource is considered fresh + """ + self.freshness = True + self.key = cache_key + self.cached_response = response + self.max_age = max_age + self.creation_time = time.time() + self.uri = request.proxy_uri + + def __str__(self): + return "Key: {key}, Fresh: {fresh}, URI: {uri}, MaxAge: {maxage}, CreationTime: {ct}, Response: {resp}"\ + .format(key=str(self.key), fresh=self.freshness, uri=self.uri, maxage=self.max_age, ct=self.creation_time, + resp=str(self.cached_response)) + +""" +class for the key used to search elements in the cache (forward-proxy only) +""" + + +class CacheKey(object): + def __init__(self, request): + """ + + :param request: + """ + logger.debug("Creating Key") + self._payload = request.payload + self._method = request.code + + """ + making a list of the options that do not have a nocachekey option number and are not uri-path, uri-host, uri-port, uri-query + """ + + self._options = [] + for option in request.options: + if (utils.check_nocachekey(option) is False) and (utils.is_uri_option(option.number) is False): + self._options.append(option) + + """ + creating a usable key for the cache structure + """ + + option_str = ', '.join(map(str, self._options)) + self._list = [self._payload, self._method, option_str] + + self.hashkey = ', '.join(map(str, self._list)) + logger.debug(self) + + def __str__(self): + msg = "" + for opt in self._options: + msg += "{name}: {value}, ".format(name=opt.name, value=opt.value) + return "Payload: {p}, Method: {m}, Options: [{o}]".format(p=self._payload, m=self._method, o=msg) + +""" +class for the key used to search elements in the cache (reverse-proxy only) +""" + + +class ReverseCacheKey(object): + def __init__(self, request): + """ + + :param request: + """ + self._payload = request.payload + self._method = request.code + + """ + making a list of the options that do not have a nocachekey option number + """ + + self._options = [] + for option in request.options: + if utils.check_nocachekey(option) is False: + self._options.append(option) + + """ + creating a usable key for the cache structure + """ + + option_str = ', '.join(map(str, self._options)) + self.hashkey = (self._payload, self._method, option_str) + + def __str__(self): + msg = "" + for opt in self._options: + msg += "{name}: {value}, ".format(name=opt.name, value=opt.value) + return "Payload: {p}, Method: {m}, Options: [{o}]".format(p=self._payload, m=self._method, o=msg) + + + + diff --git a/coapthon/caching/cache.py.bak b/coapthon/caching/cache.py.bak new file mode 100644 index 0000000..0e2391a --- /dev/null +++ b/coapthon/caching/cache.py.bak @@ -0,0 +1,246 @@ +import time + +import logging + +from coaplrucache import CoapLRUCache +from coapthon import utils +from coapthon.messages.request import * + +__author__ = 'Emilio Vallati' + +logger = logging.getLogger(__name__) + + +class Cache(object): + + def __init__(self, mode, max_dim): + """ + + :param max_dim: max number of elements in the cache + :param mode: used to differentiate between a cache used in a forward-proxy or in a reverse-proxy + """ + + self.max_dimension = max_dim + self.mode = mode + self.cache = CoapLRUCache(max_dim) + + def cache_add(self, request, response): + """ + checks for full cache and valid code before updating the cache + + :param request: + :param response: + :return: + """ + logger.debug("adding response to the cache") + + """ + checking for valid code +- """ + code = response.code + try: + utils.check_code(code) + except utils.InvalidResponseCode: # pragma no cover + logger.error("Invalid response code") + return + + """ + return if max_age is 0 + """ + if response.max_age == 0: + return + + """ + Initialising new cache element based on the mode and updating the cache + """ + + if self.mode == defines.FORWARD_PROXY: + new_key = CacheKey(request) + else: + new_key = ReverseCacheKey(request) + + logger.debug("MaxAge = {maxage}".format(maxage=response.max_age)) + new_element = CacheElement(new_key, response, request, response.max_age) + + self.cache.update(new_key, new_element) + logger.debug("Cache Size = {size}".format(size=self.cache.debug_print())) + + def search_related(self, request): + logger.debug("Cache Search Request") + if self.cache.is_empty() is True: + logger.debug("Empty Cache") + return None + + """ + extracting everything from the cache + """ + result = [] + items = self.cache.cache.items() + + for key, item in items: + element = self.cache.get(item.key) + logger.debug("Element : {elm}".format(elm=str(element))) + + if request.proxy_uri == element.uri: + result.append(item) + + return result + + def search_response(self, request): + """ + creates a key from the request and searches the cache with it + + :param request: + :return CacheElement: returns None if there's a cache miss + """ + logger.debug("Cache Search Response") + + if self.cache.is_empty() is True: + logger.debug("Empty Cache") + return None + + """ + create a new cache key from the request + """ + + if self.mode == defines.FORWARD_PROXY: + search_key = CacheKey(request) + else: + search_key = ReverseCacheKey(request) + + response = self.cache.get(search_key) + + return response + + def validate(self, request, response): + """ + refreshes a resource when a validation response is received + + :param request: + :param response: + :return: + """ + element = self.search_response(request) + if element is not None: + element.cached_response.options = response.options + element.freshness = True + element.max_age = response.max_age + element.creation_time = time.time() + element.uri = request.proxy_uri + + def mark(self, element): + """ + marks the requested resource in the cache as not fresh + :param element: + :return: + """ + logger.debug("Element : {elm}".format(elm=str(element))) + if element is not None: + logger.debug("Mark as not fresh") + element.freshness = False + logger.debug(str(self.cache)) + +""" +class for the element contained in the cache +""" + + +class CacheElement(object): + def __init__(self, cache_key, response, request, max_age=60): + """ + + :param cache_key: the key used to search for the element in the cache + :param response: the server response to store + :param max_age: maximum number of seconds that the resource is considered fresh + """ + self.freshness = True + self.key = cache_key + self.cached_response = response + self.max_age = max_age + self.creation_time = time.time() + self.uri = request.proxy_uri + + def __str__(self): + return "Key: {key}, Fresh: {fresh}, URI: {uri}, MaxAge: {maxage}, CreationTime: {ct}, Response: {resp}"\ + .format(key=str(self.key), fresh=self.freshness, uri=self.uri, maxage=self.max_age, ct=self.creation_time, + resp=str(self.cached_response)) + +""" +class for the key used to search elements in the cache (forward-proxy only) +""" + + +class CacheKey(object): + def __init__(self, request): + """ + + :param request: + """ + logger.debug("Creating Key") + self._payload = request.payload + self._method = request.code + + """ + making a list of the options that do not have a nocachekey option number and are not uri-path, uri-host, uri-port, uri-query + """ + + self._options = [] + for option in request.options: + if (utils.check_nocachekey(option) is False) and (utils.is_uri_option(option.number) is False): + self._options.append(option) + + """ + creating a usable key for the cache structure + """ + + option_str = ', '.join(map(str, self._options)) + self._list = [self._payload, self._method, option_str] + + self.hashkey = ', '.join(map(str, self._list)) + logger.debug(self) + + def __str__(self): + msg = "" + for opt in self._options: + msg += "{name}: {value}, ".format(name=opt.name, value=opt.value) + return "Payload: {p}, Method: {m}, Options: [{o}]".format(p=self._payload, m=self._method, o=msg) + +""" +class for the key used to search elements in the cache (reverse-proxy only) +""" + + +class ReverseCacheKey(object): + def __init__(self, request): + """ + + :param request: + """ + self._payload = request.payload + self._method = request.code + + """ + making a list of the options that do not have a nocachekey option number + """ + + self._options = [] + for option in request.options: + if utils.check_nocachekey(option) is False: + self._options.append(option) + + """ + creating a usable key for the cache structure + """ + + option_str = ', '.join(map(str, self._options)) + self.hashkey = (self._payload, self._method, option_str) + + def __str__(self): + msg = "" + for opt in self._options: + msg += "{name}: {value}, ".format(name=opt.name, value=opt.value) + return "Payload: {p}, Method: {m}, Options: [{o}]".format(p=self._payload, m=self._method, o=msg) + + + + diff --git a/coapthon/caching/coapcache.py b/coapthon/caching/coapcache.py new file mode 100644 index 0000000..09dc6bc --- /dev/null +++ b/coapthon/caching/coapcache.py @@ -0,0 +1,48 @@ +__author__ = 'Emilio Vallati' + + +class CoapCache: + def __init__(self, max_dim): + """ + + :param max_dim: + """ + self.cache = None + + def update(self, key, element): + """ + + :param key: + :param element: + :return: + """ + raise NotImplementedError + + def get(self, key): + """ + + :param key: + :return: CacheElement + """ + raise NotImplementedError + + def is_full(self): + """ + + :return: + """ + raise NotImplementedError + + def is_empty(self): + """ + + :return: + """ + raise NotImplementedError + + def debug_print(self): + """ + + :return: + """ + raise NotImplementedError diff --git a/coapthon/caching/coaplrucache.py b/coapthon/caching/coaplrucache.py new file mode 100644 index 0000000..3a536ef --- /dev/null +++ b/coapthon/caching/coaplrucache.py @@ -0,0 +1,83 @@ +import logging + +from cachetools import LRUCache +from coapthon.caching.coapcache import CoapCache + +__author__ = 'Emilio Vallati' + +logger = logging.getLogger(__name__) + + +class CoapLRUCache(CoapCache): + def __init__(self, max_dim): + """ + + :param max_dim: + """ + self.cache = LRUCache(maxsize=max_dim) + + def update(self, key, element): + """ + + :param key: + :param element: + :return: + """ + logger.debug("updating cache, key: %s, element: %s", \ + key.hashkey, element) + self.cache.update([(key.hashkey, element)]) + + def get(self, key): + """ + + :param key: + :return: CacheElement + """ + try: + response = self.cache[key.hashkey] + except KeyError: + logger.debug("problem here", exc_info=1) + response = None + return response + + def is_full(self): + """ + :return: + """ + if self.cache.currsize == self.cache.maxsize: + return True + return False + + def is_empty(self): + """ + + :return: + """ + + if self.cache.currsize == 0: + return True + return False + + def __str__(self): + msg = [] + for e in list(self.cache.values()): + msg.append(str(e)) + return "Cache Size: {sz}\n" + "\n".join(msg) + + def debug_print(self): + """ + + :return: + """ + return ("size = %s\n%s" % ( + self.cache.currsize, + '\n'.join([ + ( "element.max age %s\n"\ + "element.uri %s\n"\ + "element.freshness %s" ) % ( + element.max_age, + element.uri, + element.freshness ) + for key, element + in list(self.cache.items()) + ]))) diff --git a/coapthon/caching/coaplrucache.py.bak b/coapthon/caching/coaplrucache.py.bak new file mode 100644 index 0000000..dcdd745 --- /dev/null +++ b/coapthon/caching/coaplrucache.py.bak @@ -0,0 +1,83 @@ +import logging + +from cachetools import LRUCache +from coapthon.caching.coapcache import CoapCache + +__author__ = 'Emilio Vallati' + +logger = logging.getLogger(__name__) + + +class CoapLRUCache(CoapCache): + def __init__(self, max_dim): + """ + + :param max_dim: + """ + self.cache = LRUCache(maxsize=max_dim) + + def update(self, key, element): + """ + + :param key: + :param element: + :return: + """ + logger.debug("updating cache, key: %s, element: %s", \ + key.hashkey, element) + self.cache.update([(key.hashkey, element)]) + + def get(self, key): + """ + + :param key: + :return: CacheElement + """ + try: + response = self.cache[key.hashkey] + except KeyError: + logger.debug("problem here", exc_info=1) + response = None + return response + + def is_full(self): + """ + :return: + """ + if self.cache.currsize == self.cache.maxsize: + return True + return False + + def is_empty(self): + """ + + :return: + """ + + if self.cache.currsize == 0: + return True + return False + + def __str__(self): + msg = [] + for e in self.cache.values(): + msg.append(str(e)) + return "Cache Size: {sz}\n" + "\n".join(msg) + + def debug_print(self): + """ + + :return: + """ + return ("size = %s\n%s" % ( + self.cache.currsize, + '\n'.join([ + ( "element.max age %s\n"\ + "element.uri %s\n"\ + "element.freshness %s" ) % ( + element.max_age, + element.uri, + element.freshness ) + for key, element + in list(self.cache.items()) + ]))) diff --git a/coapthon/client/__init__.py b/coapthon/client/__init__.py new file mode 100644 index 0000000..0dc55fa --- /dev/null +++ b/coapthon/client/__init__.py @@ -0,0 +1 @@ +__author__ = 'Giacomo Tanganelli' diff --git a/coapthon/client/coap.py b/coapthon/client/coap.py new file mode 100644 index 0000000..8627f8e --- /dev/null +++ b/coapthon/client/coap.py @@ -0,0 +1,318 @@ +import logging.config +import os +import random +import socket +import threading +import time + +from coapthon import defines +from coapthon.layers.blocklayer import BlockLayer +from coapthon.layers.messagelayer import MessageLayer +from coapthon.layers.observelayer import ObserveLayer +from coapthon.layers.requestlayer import RequestLayer +from coapthon.messages.message import Message +from coapthon.messages.request import Request +from coapthon.messages.response import Response +from coapthon.serializer import Serializer +from coapthon.utils import create_logging +import collections + + +__author__ = 'Giacomo Tanganelli' + + +if not os.path.isfile("logging.conf"): + create_logging() + +logger = logging.getLogger(__name__) +logging.config.fileConfig("logging.conf", disable_existing_loggers=False) + + +class CoAP(object): + """ + Client class to perform requests to remote servers. + """ + def __init__(self, server, starting_mid, callback, sock=None, cb_ignore_read_exception=None, cb_ignore_write_exception=None): + """ + Initialize the client. + + :param server: Server address for incoming connections + :param callback:the callback function to be invoked when a response is received + :param starting_mid: used for testing purposes + :param sock: if a socket has been created externally, it can be used directly + :param cb_ignore_read_exception: Callback function to handle exception raised during the socket read operation + :param cb_ignore_write_exception: Callback function to handle exception raised during the socket write operation + """ + self._currentMID = starting_mid + self._server = server + self._callback = callback + self._cb_ignore_read_exception = cb_ignore_read_exception + self._cb_ignore_write_exception = cb_ignore_write_exception + self.stopped = threading.Event() + self.to_be_stopped = [] + + self._messageLayer = MessageLayer(self._currentMID) + self._blockLayer = BlockLayer() + self._observeLayer = ObserveLayer() + self._requestLayer = RequestLayer(self) + + addrinfo = socket.getaddrinfo(self._server[0], None)[0] + + if sock is not None: + self._socket = sock + + elif addrinfo[0] == socket.AF_INET: + self._socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + + else: + self._socket = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM) + self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + + self._receiver_thread = None + + def close(self): + """ + Stop the client. + + """ + self.stopped.set() + for event in self.to_be_stopped: + event.set() + if self._receiver_thread is not None: + self._receiver_thread.join() + self._socket.close() + + @property + def current_mid(self): + """ + Return the current MID. + + :return: the current mid + """ + return self._currentMID + + @current_mid.setter + def current_mid(self, c): + """ + Set the current MID. + + :param c: the mid to set + """ + assert isinstance(c, int) + self._currentMID = c + + def send_message(self, message): + """ + Prepare a message to send on the UDP socket. Eventually set retransmissions. + + :param message: the message to send + """ + if isinstance(message, Request): + request = self._requestLayer.send_request(message) + request = self._observeLayer.send_request(request) + request = self._blockLayer.send_request(request) + transaction = self._messageLayer.send_request(request) + self.send_datagram(transaction.request) + if transaction.request.type == defines.Types["CON"]: + self._start_retransmission(transaction, transaction.request) + elif isinstance(message, Message): + message = self._observeLayer.send_empty(message) + message = self._messageLayer.send_empty(None, None, message) + self.send_datagram(message) + + @staticmethod + def _wait_for_retransmit_thread(transaction): + """ + Only one retransmit thread at a time, wait for other to finish + + """ + if hasattr(transaction, 'retransmit_thread'): + while transaction.retransmit_thread is not None: + logger.debug("Waiting for retransmit thread to finish ...") + time.sleep(0.01) + continue + + def _send_block_request(self, transaction): + """ + A former request resulted in a block wise transfer. With this method, the block wise transfer + will be continued, including triggering of the retry mechanism. + + :param transaction: The former transaction including the request which should be continued. + """ + transaction = self._messageLayer.send_request(transaction.request) + # ... but don't forget to reset the acknowledge flag + transaction.request.acknowledged = False + self.send_datagram(transaction.request) + if transaction.request.type == defines.Types["CON"]: + self._start_retransmission(transaction, transaction.request) + + def send_datagram(self, message): + """ + Send a message over the UDP socket. + + :param message: the message to send + """ + host, port = message.destination + logger.debug("send_datagram - " + str(message)) + serializer = Serializer() + raw_message = serializer.serialize(message) + + try: + self._socket.sendto(raw_message, (host, port)) + except Exception as e: + if self._cb_ignore_write_exception is not None and isinstance(self._cb_ignore_write_exception, collections.Callable): + if not self._cb_ignore_write_exception(e, self): + raise + + if self._receiver_thread is None or not self._receiver_thread.isAlive(): + self._receiver_thread = threading.Thread(target=self.receive_datagram) + self._receiver_thread.start() + + def _start_retransmission(self, transaction, message): + """ + Start the retransmission task. + + :type transaction: Transaction + :param transaction: the transaction that owns the message that needs retransmission + :type message: Message + :param message: the message that needs the retransmission task + """ + with transaction: + if message.type == defines.Types['CON']: + future_time = random.uniform(defines.ACK_TIMEOUT, (defines.ACK_TIMEOUT * defines.ACK_RANDOM_FACTOR)) + transaction.retransmit_stop = threading.Event() + self.to_be_stopped.append(transaction.retransmit_stop) + transaction.retransmit_thread = threading.Thread(target=self._retransmit, + name=str('%s-Retry-%d' % (threading.current_thread().name, message.mid)), + args=(transaction, message, future_time, 0)) + transaction.retransmit_thread.start() + + def _retransmit(self, transaction, message, future_time, retransmit_count): + """ + Thread function to retransmit the message in the future + + :param transaction: the transaction that owns the message that needs retransmission + :param message: the message that needs the retransmission task + :param future_time: the amount of time to wait before a new attempt + :param retransmit_count: the number of retransmissions + """ + with transaction: + logger.debug("retransmit loop ... enter") + while retransmit_count <= defines.MAX_RETRANSMIT \ + and (not message.acknowledged and not message.rejected) \ + and not transaction.retransmit_stop.isSet(): + transaction.retransmit_stop.wait(timeout=future_time) + if not message.acknowledged and not message.rejected and not transaction.retransmit_stop.isSet(): + retransmit_count += 1 + future_time *= 2 + if retransmit_count < defines.MAX_RETRANSMIT: + logger.debug("retransmit loop ... retransmit Request") + self.send_datagram(message) + + if message.acknowledged or message.rejected: + message.timeouted = False + else: + logger.warning("Give up on message {message}".format(message=message.line_print)) + message.timeouted = True + + # Inform the user, that nothing was received + self._callback(None) + + try: + self.to_be_stopped.remove(transaction.retransmit_stop) + except ValueError: + pass + transaction.retransmit_stop = None + transaction.retransmit_thread = None + + logger.debug("retransmit loop ... exit") + + def receive_datagram(self): + """ + Receive datagram from the UDP socket and invoke the callback function. + """ + logger.debug("Start receiver Thread") + while not self.stopped.isSet(): + self._socket.settimeout(0.1) + try: + datagram, addr = self._socket.recvfrom(1152) + except socket.timeout: # pragma: no cover + continue + except Exception as e: # pragma: no cover + if self._cb_ignore_read_exception is not None and isinstance(self._cb_ignore_read_exception, collections.Callable): + if self._cb_ignore_read_exception(e, self): + continue + return + else: # pragma: no cover + if len(datagram) == 0: + logger.debug("Exiting receiver Thread due to orderly shutdown on server end") + return + + serializer = Serializer() + + try: + host, port = addr + except ValueError: + host, port, tmp1, tmp2 = addr + + source = (host, port) + + message = serializer.deserialize(datagram, source) + + if isinstance(message, Response): + logger.debug("receive_datagram - " + str(message)) + transaction, send_ack = self._messageLayer.receive_response(message) + if transaction is None: # pragma: no cover + continue + self._wait_for_retransmit_thread(transaction) + if send_ack: + self._send_ack(transaction) + self._blockLayer.receive_response(transaction) + if transaction.block_transfer: + self._send_block_request(transaction) + continue + elif transaction is None: # pragma: no cover + self._send_rst(transaction) + return + self._observeLayer.receive_response(transaction) + if transaction.notification: # pragma: no cover + ack = Message() + ack.type = defines.Types['ACK'] + ack = self._messageLayer.send_empty(transaction, transaction.response, ack) + self.send_datagram(ack) + self._callback(transaction.response) + else: + self._callback(transaction.response) + elif isinstance(message, Message): + self._messageLayer.receive_empty(message) + + logger.debug("Exiting receiver Thread due to request") + + def _send_ack(self, transaction): + """ + Sends an ACK message for the response. + + :param transaction: transaction that holds the response + """ + + ack = Message() + ack.type = defines.Types['ACK'] + + if not transaction.response.acknowledged: + ack = self._messageLayer.send_empty(transaction, transaction.response, ack) + self.send_datagram(ack) + + def _send_rst(self, transaction): # pragma: no cover + """ + Sends an RST message for the response. + + :param transaction: transaction that holds the response + """ + + rst = Message() + rst.type = defines.Types['RST'] + + if not transaction.response.acknowledged: + rst = self._messageLayer.send_empty(transaction, transaction.response, rst) + self.send_datagram(rst) diff --git a/coapthon/client/coap.py.bak b/coapthon/client/coap.py.bak new file mode 100644 index 0000000..3d2a5ba --- /dev/null +++ b/coapthon/client/coap.py.bak @@ -0,0 +1,317 @@ +import logging.config +import os +import random +import socket +import threading +import time + +from coapthon import defines +from coapthon.layers.blocklayer import BlockLayer +from coapthon.layers.messagelayer import MessageLayer +from coapthon.layers.observelayer import ObserveLayer +from coapthon.layers.requestlayer import RequestLayer +from coapthon.messages.message import Message +from coapthon.messages.request import Request +from coapthon.messages.response import Response +from coapthon.serializer import Serializer +from coapthon.utils import create_logging + + +__author__ = 'Giacomo Tanganelli' + + +if not os.path.isfile("logging.conf"): + create_logging() + +logger = logging.getLogger(__name__) +logging.config.fileConfig("logging.conf", disable_existing_loggers=False) + + +class CoAP(object): + """ + Client class to perform requests to remote servers. + """ + def __init__(self, server, starting_mid, callback, sock=None, cb_ignore_read_exception=None, cb_ignore_write_exception=None): + """ + Initialize the client. + + :param server: Server address for incoming connections + :param callback:the callback function to be invoked when a response is received + :param starting_mid: used for testing purposes + :param sock: if a socket has been created externally, it can be used directly + :param cb_ignore_read_exception: Callback function to handle exception raised during the socket read operation + :param cb_ignore_write_exception: Callback function to handle exception raised during the socket write operation + """ + self._currentMID = starting_mid + self._server = server + self._callback = callback + self._cb_ignore_read_exception = cb_ignore_read_exception + self._cb_ignore_write_exception = cb_ignore_write_exception + self.stopped = threading.Event() + self.to_be_stopped = [] + + self._messageLayer = MessageLayer(self._currentMID) + self._blockLayer = BlockLayer() + self._observeLayer = ObserveLayer() + self._requestLayer = RequestLayer(self) + + addrinfo = socket.getaddrinfo(self._server[0], None)[0] + + if sock is not None: + self._socket = sock + + elif addrinfo[0] == socket.AF_INET: + self._socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + + else: + self._socket = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM) + self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + + self._receiver_thread = None + + def close(self): + """ + Stop the client. + + """ + self.stopped.set() + for event in self.to_be_stopped: + event.set() + if self._receiver_thread is not None: + self._receiver_thread.join() + self._socket.close() + + @property + def current_mid(self): + """ + Return the current MID. + + :return: the current mid + """ + return self._currentMID + + @current_mid.setter + def current_mid(self, c): + """ + Set the current MID. + + :param c: the mid to set + """ + assert isinstance(c, int) + self._currentMID = c + + def send_message(self, message): + """ + Prepare a message to send on the UDP socket. Eventually set retransmissions. + + :param message: the message to send + """ + if isinstance(message, Request): + request = self._requestLayer.send_request(message) + request = self._observeLayer.send_request(request) + request = self._blockLayer.send_request(request) + transaction = self._messageLayer.send_request(request) + self.send_datagram(transaction.request) + if transaction.request.type == defines.Types["CON"]: + self._start_retransmission(transaction, transaction.request) + elif isinstance(message, Message): + message = self._observeLayer.send_empty(message) + message = self._messageLayer.send_empty(None, None, message) + self.send_datagram(message) + + @staticmethod + def _wait_for_retransmit_thread(transaction): + """ + Only one retransmit thread at a time, wait for other to finish + + """ + if hasattr(transaction, 'retransmit_thread'): + while transaction.retransmit_thread is not None: + logger.debug("Waiting for retransmit thread to finish ...") + time.sleep(0.01) + continue + + def _send_block_request(self, transaction): + """ + A former request resulted in a block wise transfer. With this method, the block wise transfer + will be continued, including triggering of the retry mechanism. + + :param transaction: The former transaction including the request which should be continued. + """ + transaction = self._messageLayer.send_request(transaction.request) + # ... but don't forget to reset the acknowledge flag + transaction.request.acknowledged = False + self.send_datagram(transaction.request) + if transaction.request.type == defines.Types["CON"]: + self._start_retransmission(transaction, transaction.request) + + def send_datagram(self, message): + """ + Send a message over the UDP socket. + + :param message: the message to send + """ + host, port = message.destination + logger.debug("send_datagram - " + str(message)) + serializer = Serializer() + raw_message = serializer.serialize(message) + + try: + self._socket.sendto(raw_message, (host, port)) + except Exception as e: + if self._cb_ignore_write_exception is not None and callable(self._cb_ignore_write_exception): + if not self._cb_ignore_write_exception(e, self): + raise + + if self._receiver_thread is None or not self._receiver_thread.isAlive(): + self._receiver_thread = threading.Thread(target=self.receive_datagram) + self._receiver_thread.start() + + def _start_retransmission(self, transaction, message): + """ + Start the retransmission task. + + :type transaction: Transaction + :param transaction: the transaction that owns the message that needs retransmission + :type message: Message + :param message: the message that needs the retransmission task + """ + with transaction: + if message.type == defines.Types['CON']: + future_time = random.uniform(defines.ACK_TIMEOUT, (defines.ACK_TIMEOUT * defines.ACK_RANDOM_FACTOR)) + transaction.retransmit_stop = threading.Event() + self.to_be_stopped.append(transaction.retransmit_stop) + transaction.retransmit_thread = threading.Thread(target=self._retransmit, + name=str('%s-Retry-%d' % (threading.current_thread().name, message.mid)), + args=(transaction, message, future_time, 0)) + transaction.retransmit_thread.start() + + def _retransmit(self, transaction, message, future_time, retransmit_count): + """ + Thread function to retransmit the message in the future + + :param transaction: the transaction that owns the message that needs retransmission + :param message: the message that needs the retransmission task + :param future_time: the amount of time to wait before a new attempt + :param retransmit_count: the number of retransmissions + """ + with transaction: + logger.debug("retransmit loop ... enter") + while retransmit_count <= defines.MAX_RETRANSMIT \ + and (not message.acknowledged and not message.rejected) \ + and not transaction.retransmit_stop.isSet(): + transaction.retransmit_stop.wait(timeout=future_time) + if not message.acknowledged and not message.rejected and not transaction.retransmit_stop.isSet(): + retransmit_count += 1 + future_time *= 2 + if retransmit_count < defines.MAX_RETRANSMIT: + logger.debug("retransmit loop ... retransmit Request") + self.send_datagram(message) + + if message.acknowledged or message.rejected: + message.timeouted = False + else: + logger.warning("Give up on message {message}".format(message=message.line_print)) + message.timeouted = True + + # Inform the user, that nothing was received + self._callback(None) + + try: + self.to_be_stopped.remove(transaction.retransmit_stop) + except ValueError: + pass + transaction.retransmit_stop = None + transaction.retransmit_thread = None + + logger.debug("retransmit loop ... exit") + + def receive_datagram(self): + """ + Receive datagram from the UDP socket and invoke the callback function. + """ + logger.debug("Start receiver Thread") + while not self.stopped.isSet(): + self._socket.settimeout(0.1) + try: + datagram, addr = self._socket.recvfrom(1152) + except socket.timeout: # pragma: no cover + continue + except Exception as e: # pragma: no cover + if self._cb_ignore_read_exception is not None and callable(self._cb_ignore_read_exception): + if self._cb_ignore_read_exception(e, self): + continue + return + else: # pragma: no cover + if len(datagram) == 0: + logger.debug("Exiting receiver Thread due to orderly shutdown on server end") + return + + serializer = Serializer() + + try: + host, port = addr + except ValueError: + host, port, tmp1, tmp2 = addr + + source = (host, port) + + message = serializer.deserialize(datagram, source) + + if isinstance(message, Response): + logger.debug("receive_datagram - " + str(message)) + transaction, send_ack = self._messageLayer.receive_response(message) + if transaction is None: # pragma: no cover + continue + self._wait_for_retransmit_thread(transaction) + if send_ack: + self._send_ack(transaction) + self._blockLayer.receive_response(transaction) + if transaction.block_transfer: + self._send_block_request(transaction) + continue + elif transaction is None: # pragma: no cover + self._send_rst(transaction) + return + self._observeLayer.receive_response(transaction) + if transaction.notification: # pragma: no cover + ack = Message() + ack.type = defines.Types['ACK'] + ack = self._messageLayer.send_empty(transaction, transaction.response, ack) + self.send_datagram(ack) + self._callback(transaction.response) + else: + self._callback(transaction.response) + elif isinstance(message, Message): + self._messageLayer.receive_empty(message) + + logger.debug("Exiting receiver Thread due to request") + + def _send_ack(self, transaction): + """ + Sends an ACK message for the response. + + :param transaction: transaction that holds the response + """ + + ack = Message() + ack.type = defines.Types['ACK'] + + if not transaction.response.acknowledged: + ack = self._messageLayer.send_empty(transaction, transaction.response, ack) + self.send_datagram(ack) + + def _send_rst(self, transaction): # pragma: no cover + """ + Sends an RST message for the response. + + :param transaction: transaction that holds the response + """ + + rst = Message() + rst.type = defines.Types['RST'] + + if not transaction.response.acknowledged: + rst = self._messageLayer.send_empty(transaction, transaction.response, rst) + self.send_datagram(rst) diff --git a/coapthon/client/helperclient.py b/coapthon/client/helperclient.py new file mode 100644 index 0000000..986be8b --- /dev/null +++ b/coapthon/client/helperclient.py @@ -0,0 +1,236 @@ +import random +from multiprocessing import Queue +from queue import Empty +import threading +from coapthon.messages.message import Message +from coapthon import defines +from coapthon.client.coap import CoAP +from coapthon.messages.request import Request +from coapthon.utils import generate_random_token + +__author__ = 'Giacomo Tanganelli' + + +class HelperClient(object): + """ + Helper Client class to perform requests to remote servers in a simplified way. + """ + def __init__(self, server, sock=None, cb_ignore_read_exception=None, cb_ignore_write_exception=None): + """ + Initialize a client to perform request to a server. + + :param server: the remote CoAP server + :param sock: if a socket has been created externally, it can be used directly + :param cb_ignore_read_exception: Callback function to handle exception raised during the socket read operation + :param cb_ignore_write_exception: Callback function to handle exception raised during the socket write operation + """ + self.server = server + self.protocol = CoAP(self.server, random.randint(1, 65535), self._wait_response, sock=sock, + cb_ignore_read_exception=cb_ignore_read_exception, cb_ignore_write_exception=cb_ignore_write_exception) + self.queue = Queue() + + def _wait_response(self, message): + """ + Private function to get responses from the server. + + :param message: the received message + """ + if message is None or message.code != defines.Codes.CONTINUE.number: + self.queue.put(message) + + def stop(self): + """ + Stop the client. + """ + self.protocol.close() + self.queue.put(None) + + def close(self): + """ + Close the client. + """ + self.stop() + + def _thread_body(self, request, callback): + """ + Private function. Send a request, wait for response and call the callback function. + + :param request: the request to send + :param callback: the callback function + """ + self.protocol.send_message(request) + while not self.protocol.stopped.isSet(): + response = self.queue.get(block=True) + callback(response) + + def cancel_observing(self, response, send_rst): # pragma: no cover + """ + Delete observing on the remote server. + + :param response: the last received response + :param send_rst: if explicitly send RST message + :type send_rst: bool + """ + if send_rst: + message = Message() + message.destination = self.server + message.code = defines.Codes.EMPTY.number + message.type = defines.Types["RST"] + message.token = response.token + message.mid = response.mid + self.protocol.send_message(message) + self.stop() + + def get(self, path, callback=None, timeout=None, **kwargs): # pragma: no cover + """ + Perform a GET on a certain path. + + :param path: the path + :param callback: the callback function to invoke upon response + :param timeout: the timeout of the request + :return: the response + """ + request = self.mk_request(defines.Codes.GET, path) + request.token = generate_random_token(2) + + for k, v in kwargs.items(): + if hasattr(request, k): + setattr(request, k, v) + + return self.send_request(request, callback, timeout) + + def observe(self, path, callback, timeout=None, **kwargs): # pragma: no cover + """ + Perform a GET with observe on a certain path. + + :param path: the path + :param callback: the callback function to invoke upon notifications + :param timeout: the timeout of the request + :return: the response to the observe request + """ + request = self.mk_request(defines.Codes.GET, path) + request.observe = 0 + + for k, v in kwargs.items(): + if hasattr(request, k): + setattr(request, k, v) + + return self.send_request(request, callback, timeout) + + def delete(self, path, callback=None, timeout=None, **kwargs): # pragma: no cover + """ + Perform a DELETE on a certain path. + + :param path: the path + :param callback: the callback function to invoke upon response + :param timeout: the timeout of the request + :return: the response + """ + request = self.mk_request(defines.Codes.DELETE, path) + + for k, v in kwargs.items(): + if hasattr(request, k): + setattr(request, k, v) + + return self.send_request(request, callback, timeout) + + def post(self, path, payload, callback=None, timeout=None, **kwargs): # pragma: no cover + """ + Perform a POST on a certain path. + + :param path: the path + :param payload: the request payload + :param callback: the callback function to invoke upon response + :param timeout: the timeout of the request + :return: the response + """ + request = self.mk_request(defines.Codes.POST, path) + request.token = generate_random_token(2) + request.payload = payload + + for k, v in kwargs.items(): + if hasattr(request, k): + setattr(request, k, v) + + return self.send_request(request, callback, timeout) + + def put(self, path, payload, callback=None, timeout=None, **kwargs): # pragma: no cover + """ + Perform a PUT on a certain path. + + :param path: the path + :param payload: the request payload + :param callback: the callback function to invoke upon response + :param timeout: the timeout of the request + :return: the response + """ + request = self.mk_request(defines.Codes.PUT, path) + request.token = generate_random_token(2) + request.payload = payload + + for k, v in kwargs.items(): + if hasattr(request, k): + setattr(request, k, v) + + return self.send_request(request, callback, timeout) + + def discover(self, callback=None, timeout=None, **kwargs): # pragma: no cover + """ + Perform a Discover request on the server. + + :param callback: the callback function to invoke upon response + :param timeout: the timeout of the request + :return: the response + """ + request = self.mk_request(defines.Codes.GET, defines.DISCOVERY_URL) + + for k, v in kwargs.items(): + if hasattr(request, k): + setattr(request, k, v) + + return self.send_request(request, callback, timeout) + + def send_request(self, request, callback=None, timeout=None): # pragma: no cover + """ + Send a request to the remote server. + + :param request: the request to send + :param callback: the callback function to invoke upon response + :param timeout: the timeout of the request + :return: the response + """ + if callback is not None: + thread = threading.Thread(target=self._thread_body, args=(request, callback)) + thread.start() + else: + self.protocol.send_message(request) + try: + response = self.queue.get(block=True, timeout=timeout) + except Empty: + #if timeout is set + response = None + return response + + def send_empty(self, empty): # pragma: no cover + """ + Send empty message. + + :param empty: the empty message + """ + self.protocol.send_message(empty) + + def mk_request(self, method, path): + """ + Create a request. + + :param method: the CoAP method + :param path: the path of the request + :return: the request + """ + request = Request() + request.destination = self.server + request.code = method.number + request.uri_path = path + return request + + diff --git a/coapthon/client/helperclient.py.bak b/coapthon/client/helperclient.py.bak new file mode 100644 index 0000000..edbaa13 --- /dev/null +++ b/coapthon/client/helperclient.py.bak @@ -0,0 +1,236 @@ +import random +from multiprocessing import Queue +from Queue import Empty +import threading +from coapthon.messages.message import Message +from coapthon import defines +from coapthon.client.coap import CoAP +from coapthon.messages.request import Request +from coapthon.utils import generate_random_token + +__author__ = 'Giacomo Tanganelli' + + +class HelperClient(object): + """ + Helper Client class to perform requests to remote servers in a simplified way. + """ + def __init__(self, server, sock=None, cb_ignore_read_exception=None, cb_ignore_write_exception=None): + """ + Initialize a client to perform request to a server. + + :param server: the remote CoAP server + :param sock: if a socket has been created externally, it can be used directly + :param cb_ignore_read_exception: Callback function to handle exception raised during the socket read operation + :param cb_ignore_write_exception: Callback function to handle exception raised during the socket write operation + """ + self.server = server + self.protocol = CoAP(self.server, random.randint(1, 65535), self._wait_response, sock=sock, + cb_ignore_read_exception=cb_ignore_read_exception, cb_ignore_write_exception=cb_ignore_write_exception) + self.queue = Queue() + + def _wait_response(self, message): + """ + Private function to get responses from the server. + + :param message: the received message + """ + if message is None or message.code != defines.Codes.CONTINUE.number: + self.queue.put(message) + + def stop(self): + """ + Stop the client. + """ + self.protocol.close() + self.queue.put(None) + + def close(self): + """ + Close the client. + """ + self.stop() + + def _thread_body(self, request, callback): + """ + Private function. Send a request, wait for response and call the callback function. + + :param request: the request to send + :param callback: the callback function + """ + self.protocol.send_message(request) + while not self.protocol.stopped.isSet(): + response = self.queue.get(block=True) + callback(response) + + def cancel_observing(self, response, send_rst): # pragma: no cover + """ + Delete observing on the remote server. + + :param response: the last received response + :param send_rst: if explicitly send RST message + :type send_rst: bool + """ + if send_rst: + message = Message() + message.destination = self.server + message.code = defines.Codes.EMPTY.number + message.type = defines.Types["RST"] + message.token = response.token + message.mid = response.mid + self.protocol.send_message(message) + self.stop() + + def get(self, path, callback=None, timeout=None, **kwargs): # pragma: no cover + """ + Perform a GET on a certain path. + + :param path: the path + :param callback: the callback function to invoke upon response + :param timeout: the timeout of the request + :return: the response + """ + request = self.mk_request(defines.Codes.GET, path) + request.token = generate_random_token(2) + + for k, v in kwargs.iteritems(): + if hasattr(request, k): + setattr(request, k, v) + + return self.send_request(request, callback, timeout) + + def observe(self, path, callback, timeout=None, **kwargs): # pragma: no cover + """ + Perform a GET with observe on a certain path. + + :param path: the path + :param callback: the callback function to invoke upon notifications + :param timeout: the timeout of the request + :return: the response to the observe request + """ + request = self.mk_request(defines.Codes.GET, path) + request.observe = 0 + + for k, v in kwargs.iteritems(): + if hasattr(request, k): + setattr(request, k, v) + + return self.send_request(request, callback, timeout) + + def delete(self, path, callback=None, timeout=None, **kwargs): # pragma: no cover + """ + Perform a DELETE on a certain path. + + :param path: the path + :param callback: the callback function to invoke upon response + :param timeout: the timeout of the request + :return: the response + """ + request = self.mk_request(defines.Codes.DELETE, path) + + for k, v in kwargs.iteritems(): + if hasattr(request, k): + setattr(request, k, v) + + return self.send_request(request, callback, timeout) + + def post(self, path, payload, callback=None, timeout=None, **kwargs): # pragma: no cover + """ + Perform a POST on a certain path. + + :param path: the path + :param payload: the request payload + :param callback: the callback function to invoke upon response + :param timeout: the timeout of the request + :return: the response + """ + request = self.mk_request(defines.Codes.POST, path) + request.token = generate_random_token(2) + request.payload = payload + + for k, v in kwargs.iteritems(): + if hasattr(request, k): + setattr(request, k, v) + + return self.send_request(request, callback, timeout) + + def put(self, path, payload, callback=None, timeout=None, **kwargs): # pragma: no cover + """ + Perform a PUT on a certain path. + + :param path: the path + :param payload: the request payload + :param callback: the callback function to invoke upon response + :param timeout: the timeout of the request + :return: the response + """ + request = self.mk_request(defines.Codes.PUT, path) + request.token = generate_random_token(2) + request.payload = payload + + for k, v in kwargs.iteritems(): + if hasattr(request, k): + setattr(request, k, v) + + return self.send_request(request, callback, timeout) + + def discover(self, callback=None, timeout=None, **kwargs): # pragma: no cover + """ + Perform a Discover request on the server. + + :param callback: the callback function to invoke upon response + :param timeout: the timeout of the request + :return: the response + """ + request = self.mk_request(defines.Codes.GET, defines.DISCOVERY_URL) + + for k, v in kwargs.iteritems(): + if hasattr(request, k): + setattr(request, k, v) + + return self.send_request(request, callback, timeout) + + def send_request(self, request, callback=None, timeout=None): # pragma: no cover + """ + Send a request to the remote server. + + :param request: the request to send + :param callback: the callback function to invoke upon response + :param timeout: the timeout of the request + :return: the response + """ + if callback is not None: + thread = threading.Thread(target=self._thread_body, args=(request, callback)) + thread.start() + else: + self.protocol.send_message(request) + try: + response = self.queue.get(block=True, timeout=timeout) + except Empty: + #if timeout is set + response = None + return response + + def send_empty(self, empty): # pragma: no cover + """ + Send empty message. + + :param empty: the empty message + """ + self.protocol.send_message(empty) + + def mk_request(self, method, path): + """ + Create a request. + + :param method: the CoAP method + :param path: the path of the request + :return: the request + """ + request = Request() + request.destination = self.server + request.code = method.number + request.uri_path = path + return request + + diff --git a/coapthon/defines.py b/coapthon/defines.py new file mode 100644 index 0000000..86fa878 --- /dev/null +++ b/coapthon/defines.py @@ -0,0 +1,300 @@ +import collections +import array +import struct + +__author__ = 'Giacomo Tanganelli' + +""" CoAP Parameters """ + +ACK_TIMEOUT = 2 # standard 2 + +SEPARATE_TIMEOUT = ACK_TIMEOUT / 2 + +ACK_RANDOM_FACTOR = 1.5 + +MAX_RETRANSMIT = 1 + +MAX_TRANSMIT_SPAN = ACK_TIMEOUT * (pow(2, (MAX_RETRANSMIT + 1)) - 1) * ACK_RANDOM_FACTOR + +MAX_LATENCY = 120 # 2 minutes + +PROCESSING_DELAY = ACK_TIMEOUT + +MAX_RTT = (2 * MAX_LATENCY) + PROCESSING_DELAY + +EXCHANGE_LIFETIME = MAX_TRANSMIT_SPAN + (2 * MAX_LATENCY) + PROCESSING_DELAY + +DISCOVERY_URL = "/.well-known/core" + +ALL_COAP_NODES = "224.0.1.187" + +ALL_COAP_NODES_IPV6 = "FF00::FD" + +MAX_PAYLOAD = 1024 + +MAX_NON_NOTIFICATIONS = 10 + +BLOCKWISE_SIZE = 1024 + +""" Message Format """ + +# number of bits used for the encoding of the CoAP version field. +VERSION_BITS = 2 + +# number of bits used for the encoding of the message type field. +TYPE_BITS = 2 + +# number of bits used for the encoding of the token length field. +TOKEN_LENGTH_BITS = 4 + +# number of bits used for the encoding of the request method/response code field. +CODE_BITS = 8 + +# number of bits used for the encoding of the message ID. +MESSAGE_ID_BITS = 16 + +# number of bits used for the encoding of the option delta field. +OPTION_DELTA_BITS = 4 + +# number of bits used for the encoding of the option delta field. +OPTION_LENGTH_BITS = 4 + +# One byte which indicates indicates the end of options and the start of the payload. +PAYLOAD_MARKER = 0xFF + +# CoAP version supported by this Californium version. +VERSION = 1 + +# The lowest value of a request code. +REQUEST_CODE_LOWER_BOUND = 1 + +# The highest value of a request code. +REQUEST_CODE_UPPER_BOUND = 31 + +# The lowest value of a response code. +RESPONSE_CODE_LOWER_BOUND = 64 + +# The highest value of a response code. +RESPONSE_CODE_UPPER_BOUND = 191 + +corelinkformat = { + 'ct': 'content_type', + 'rt': 'resource_type', + 'if': 'interface_type', + 'sz': 'maximum_size_estimated', + 'obs': 'observing' +} + +# The integer. +INTEGER = 0 +# The string. +STRING = 1 +# The opaque. +OPAQUE = 2 +# The unknown. +UNKNOWN = 3 + +# Cache modes +FORWARD_PROXY = 0 +REVERSE_PROXY = 1 + +OptionItem = collections.namedtuple('OptionItem', 'number name value_type repeatable default') + + +class OptionRegistry(object): + """ + All CoAP options. Every option is represented as: (NUMBER, NAME, VALUE_TYPE, REPEATABLE, DEFAULT) + """ + def __init__(self): + pass + + RESERVED = OptionItem(0, "Reserved", UNKNOWN, True, None) + IF_MATCH = OptionItem(1, "If-Match", OPAQUE, True, None) + URI_HOST = OptionItem(3, "Uri-Host", STRING, True, None) + ETAG = OptionItem(4, "ETag", OPAQUE, True, None) + IF_NONE_MATCH = OptionItem(5, "If-None-Match", OPAQUE, False, None) + OBSERVE = OptionItem(6, "Observe", INTEGER, False, 0) + URI_PORT = OptionItem(7, "Uri-Port", INTEGER, False, 5683) + LOCATION_PATH = OptionItem(8, "Location-Path", STRING, True, None) + URI_PATH = OptionItem(11, "Uri-Path", STRING, True, None) + CONTENT_TYPE = OptionItem(12, "Content-Type", INTEGER, False, 0) + MAX_AGE = OptionItem(14, "Max-Age", INTEGER, False, 60) + URI_QUERY = OptionItem(15, "Uri-Query", STRING, True, None) + ACCEPT = OptionItem(17, "Accept", INTEGER, False, 0) + LOCATION_QUERY = OptionItem(20,"Location-Query",STRING, True, None) + BLOCK2 = OptionItem(23, "Block2", INTEGER, False, None) + BLOCK1 = OptionItem(27, "Block1", INTEGER, False, None) + PROXY_URI = OptionItem(35, "Proxy-Uri", STRING, False, None) + PROXY_SCHEME = OptionItem(39, "Proxy-Schema", STRING, False, None) + SIZE1 = OptionItem(60, "Size1", INTEGER, False, None) + RM_MESSAGE_SWITCHING = OptionItem(65524, "Routing", OPAQUE, False, None) + + LIST = { + 0: RESERVED, + 1: IF_MATCH, + 3: URI_HOST, + 4: ETAG, + 5: IF_NONE_MATCH, + 6: OBSERVE, + 7: URI_PORT, + 8: LOCATION_PATH, + 11: URI_PATH, + 12: CONTENT_TYPE, + 14: MAX_AGE, + 15: URI_QUERY, + 17: ACCEPT, + 20: LOCATION_QUERY, + 23: BLOCK2, + 27: BLOCK1, + 35: PROXY_URI, + 39: PROXY_SCHEME, + 60: SIZE1, + 65524: RM_MESSAGE_SWITCHING + + } + + @staticmethod + def get_option_flags(option_num): + """ + Get Critical, UnSafe, NoCacheKey flags from the option number + as per RFC 7252, section 5.4.6 + + :param option_num: option number + :return: option flags + :rtype: 3-tuple (critical, unsafe, no-cache) + """ + opt_bytes = array.array('B', '\0\0') + if option_num < 256: + s = struct.Struct("!B") + s.pack_into(opt_bytes, 0, option_num) + else: + s = struct.Struct("H") + s.pack_into(opt_bytes, 0, option_num) + critical = (opt_bytes[0] & 0x01) > 0 + unsafe = (opt_bytes[0] & 0x02) > 0 + nocache = ((opt_bytes[0] & 0x1e) == 0x1c) + return (critical, unsafe, nocache) + +Types = { + 'CON': 0, + 'NON': 1, + 'ACK': 2, + 'RST': 3, + 'None': None +} + +CodeItem = collections.namedtuple('CodeItem', 'number name') + + +class Codes(object): + """ + CoAP codes. Every code is represented as (NUMBER, NAME) + """ + ERROR_LOWER_BOUND = 128 + + EMPTY = CodeItem(0, 'EMPTY') + GET = CodeItem(1, 'GET') + POST = CodeItem(2, 'POST') + PUT = CodeItem(3, 'PUT') + DELETE = CodeItem(4, 'DELETE') + + CREATED = CodeItem(65, 'CREATED') + DELETED = CodeItem(66, 'DELETED') + VALID = CodeItem(67, 'VALID') + CHANGED = CodeItem(68, 'CHANGED') + CONTENT = CodeItem(69, 'CONTENT') + CONTINUE = CodeItem(95, 'CONTINUE') + + BAD_REQUEST = CodeItem(128, 'BAD_REQUEST') + FORBIDDEN = CodeItem(131, 'FORBIDDEN') + NOT_FOUND = CodeItem(132, 'NOT_FOUND') + METHOD_NOT_ALLOWED = CodeItem(133, 'METHOD_NOT_ALLOWED') + NOT_ACCEPTABLE = CodeItem(134, 'NOT_ACCEPTABLE') + REQUEST_ENTITY_INCOMPLETE = CodeItem(136, 'REQUEST_ENTITY_INCOMPLETE') + PRECONDITION_FAILED = CodeItem(140, 'PRECONDITION_FAILED') + REQUEST_ENTITY_TOO_LARGE = CodeItem(141, 'REQUEST_ENTITY_TOO_LARGE') + UNSUPPORTED_CONTENT_FORMAT = CodeItem(143, 'UNSUPPORTED_CONTENT_FORMAT') + + INTERNAL_SERVER_ERROR = CodeItem(160, 'INTERNAL_SERVER_ERROR') + NOT_IMPLEMENTED = CodeItem(161, 'NOT_IMPLEMENTED') + BAD_GATEWAY = CodeItem(162, 'BAD_GATEWAY') + SERVICE_UNAVAILABLE = CodeItem(163, 'SERVICE_UNAVAILABLE') + GATEWAY_TIMEOUT = CodeItem(164, 'GATEWAY_TIMEOUT') + PROXY_NOT_SUPPORTED = CodeItem(165, 'PROXY_NOT_SUPPORTED') + + LIST = { + 0: EMPTY, + 1: GET, + 2: POST, + 3: PUT, + 4: DELETE, + + 65: CREATED, + 66: DELETE, + 67: VALID, + 68: CHANGED, + 69: CONTENT, + 95: CONTINUE, + + 128: BAD_REQUEST, + 131: FORBIDDEN, + 132: NOT_FOUND, + 133: METHOD_NOT_ALLOWED, + 134: NOT_ACCEPTABLE, + 136: REQUEST_ENTITY_INCOMPLETE, + 140: PRECONDITION_FAILED, + 141: REQUEST_ENTITY_TOO_LARGE, + 143: UNSUPPORTED_CONTENT_FORMAT, + + 160: INTERNAL_SERVER_ERROR, + 161: NOT_IMPLEMENTED, + 162: BAD_GATEWAY, + 163: SERVICE_UNAVAILABLE, + 164: GATEWAY_TIMEOUT, + 165: PROXY_NOT_SUPPORTED + + } + + +Content_types = { + "text/plain": 0, + "application/link-format": 40, + "application/xml": 41, + "application/octet-stream": 42, + "application/exi": 47, + "application/json": 50, + "application/cbor": 60 +} + +COAP_PREFACE = "coap://" +LOCALHOST = "127.0.0.1" +HC_PROXY_DEFAULT_PORT = 8080 # TODO there is a standard for this? +COAP_DEFAULT_PORT = 5683 +DEFAULT_HC_PATH = "/" +BAD_REQUEST = 400 # "Bad Request" error code +NOT_IMPLEMENTED = 501 # "Not Implemented" error code + +# Dictionary to map CoAP to HTTP requests code +CoAP_HTTP = { + + "CREATED": "201", + "DELETED": "200", + "VALID": "304", + "CHANGED": "200", + "CONTENT": "200", + "BAD_REQUEST": "400", + "FORBIDDEN": "403", + "NOT_FOUND": "404", + "METHOD_NOT_ALLOWED": "400", + "NOT_ACCEPTABLE": "406", + "PRECONDITION_FAILED": "412", + "REQUEST_ENTITY_TOO_LARGE": "413", + "UNSUPPORTED_CONTENT_FORMAT": "415", + "INTERNAL_SERVER_ERROR": "500", + "NOT_IMPLEMENTED": "501", + "BAD_GATEWAY": "502", + "SERVICE_UNAVAILABLE": "503", + "GATEWAY_TIMEOUT": "504", + "PROXY_NOT_SUPPORTED": "502" + +} diff --git a/coapthon/forward_proxy/__init__.py b/coapthon/forward_proxy/__init__.py new file mode 100644 index 0000000..0dc55fa --- /dev/null +++ b/coapthon/forward_proxy/__init__.py @@ -0,0 +1 @@ +__author__ = 'Giacomo Tanganelli' diff --git a/coapthon/forward_proxy/coap.py b/coapthon/forward_proxy/coap.py new file mode 100644 index 0000000..5b42ddb --- /dev/null +++ b/coapthon/forward_proxy/coap.py @@ -0,0 +1,366 @@ +import logging.config +import random +import socket +import struct +import threading + +import os + +from coapthon import defines +from coapthon.layers.blocklayer import BlockLayer +from coapthon.layers.cachelayer import CacheLayer +from coapthon.layers.forwardLayer import ForwardLayer +from coapthon.layers.messagelayer import MessageLayer +from coapthon.layers.observelayer import ObserveLayer +from coapthon.layers.resourcelayer import ResourceLayer +from coapthon.messages.message import Message +from coapthon.messages.request import Request +from coapthon.resources.resource import Resource +from coapthon.serializer import Serializer +from coapthon.utils import Tree, create_logging + +__author__ = 'Giacomo Tanganelli' + +if not os.path.isfile("logging.conf"): + create_logging() + +logger = logging.getLogger(__name__) +logging.config.fileConfig("logging.conf", disable_existing_loggers=False) + + +class CoAP(object): + """ + Implementation of the Forward Proxy + """ + def __init__(self, server_address, multicast=False, starting_mid=None, cache=False, sock=None): + """ + Initialize the Forward Proxy. + + :param server_address: Server address for incoming connections + :param multicast: if the ip is a multicast address + :param starting_mid: used for testing purposes + :param cache: if a cache must be used + :param sock: if a socket has been created externally, it can be used directly + """ + self.stopped = threading.Event() + self.stopped.clear() + self.to_be_stopped = [] + self.purge = threading.Thread(target=self.purge) + self.purge.start() + self.cache_enable = cache + + self._messageLayer = MessageLayer(starting_mid) + self._blockLayer = BlockLayer() + self._observeLayer = ObserveLayer() + + if self.cache_enable: + self._cacheLayer = CacheLayer(defines.FORWARD_PROXY) + else: + self._cacheLayer = None + + self._forwardLayer = ForwardLayer(self) + self.resourceLayer = ResourceLayer(self) + + # Resource directory + root = Resource('root', self, visible=False, observable=False, allow_children=True) + root.path = '/' + self.root = Tree() + self.root["/"] = root + self._serializer = None + + self.server_address = server_address + self.multicast = multicast + + addrinfo = socket.getaddrinfo(self.server_address[0], None)[0] + + if sock is not None: + + # Use given socket, could be a DTLS socket + self._socket = sock + + elif self.multicast: # pragma: no cover + + # Create a socket + # self._socket.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_TTL, 255) + # self._socket.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_LOOP, 1) + + # Join group + if addrinfo[0] == socket.AF_INET: # IPv4 + self._socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP) + + # Allow multiple copies of this program on one machine + # (not strictly needed) + self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + self._socket.bind((defines.ALL_COAP_NODES, self.server_address[1])) + mreq = struct.pack("4sl", socket.inet_aton(defines.ALL_COAP_NODES), socket.INADDR_ANY) + self._socket.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq) + self._unicast_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + self._unicast_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + self._unicast_socket.bind(self.server_address) + else: + self._socket = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM, socket.IPPROTO_UDP) + + # Allow multiple copies of this program on one machine + # (not strictly needed) + self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + self._socket.bind((defines.ALL_COAP_NODES_IPV6, self.server_address[1])) + + addrinfo_multicast = socket.getaddrinfo(defines.ALL_COAP_NODES_IPV6, 5683)[0] + group_bin = socket.inet_pton(socket.AF_INET6, addrinfo_multicast[4][0]) + mreq = group_bin + struct.pack('@I', 0) + self._socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_JOIN_GROUP, mreq) + self._unicast_socket = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM) + self._unicast_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + self._unicast_socket.bind(self.server_address) + else: + if addrinfo[0] == socket.AF_INET: # IPv4 + self._socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + else: + self._socket = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM) + self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + + self._socket.bind(self.server_address) + + def purge(self): + """ + Clean old transactions + """ + while not self.stopped.isSet(): + self.stopped.wait(timeout=defines.EXCHANGE_LIFETIME) + self._messageLayer.purge() + + 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: + #Start a new thread not to block other requests + args = ((data, client_address), ) + t = threading.Thread(target=self.receive_datagram, args=args) + t.daemon = True + t.start() + except RuntimeError: + logging.exception("Exception with Executor") + logging.debug("closing socket") + self._socket.close() + + def close(self): + """ + Stop the server. + + """ + logger.info("Stop server") + self.stopped.set() + for event in self.to_be_stopped: + event.set() + self._socket.close() + + def receive_datagram(self, args): + """ + Handle messages coming from the udp socket. + + :param args: (data, client_address) + """ + data, client_address = args + + logging.debug("receiving datagram") + + try: + host, port = client_address + except ValueError: + host, port, tmp1, tmp2 = client_address + + client_address = (host, port) + + serializer = Serializer() + message = serializer.deserialize(data, client_address) + if isinstance(message, int): + logger.error("receive_datagram - BAD REQUEST") + + rst = Message() + rst.destination = client_address + rst.type = defines.Types["RST"] + rst.code = message + self.send_datagram(rst) + return + logger.debug("receive_datagram - " + str(message)) + if isinstance(message, Request): + + transaction = self._messageLayer.receive_request(message) + + if transaction.request.duplicated and transaction.completed: + logger.debug("message duplicated,transaction completed") + transaction = self._observeLayer.send_response(transaction) + transaction = self._blockLayer.send_response(transaction) + transaction = self._messageLayer.send_response(transaction) + self.send_datagram(transaction.response) + return + elif transaction.request.duplicated and not transaction.completed: + logger.debug("message duplicated,transaction NOT completed") + self._send_ack(transaction) + return + + transaction.separate_timer = self._start_separate_timer(transaction) + + transaction = self._blockLayer.receive_request(transaction) + + if transaction.block_transfer: + self._stop_separate_timer(transaction.separate_timer) + transaction = self._messageLayer.send_response(transaction) + self.send_datagram(transaction.response) + return + + transaction = self._observeLayer.receive_request(transaction) + + """ + call to the cache layer to check if there's a cached response for the request + if not, call the forward layer + """ + if self._cacheLayer is not None: + transaction = self._cacheLayer.receive_request(transaction) + + if transaction.cacheHit is False: + logging.debug(transaction.request) + transaction = self._forwardLayer.receive_request(transaction) + logging.debug(transaction.response) + + transaction = self._observeLayer.send_response(transaction) + + transaction = self._blockLayer.send_response(transaction) + + transaction = self._cacheLayer.send_response(transaction) + else: + transaction = self._forwardLayer.receive_request(transaction) + + transaction = self._observeLayer.send_response(transaction) + + transaction = self._blockLayer.send_response(transaction) + + self._stop_separate_timer(transaction.separate_timer) + + transaction = self._messageLayer.send_response(transaction) + + if transaction.response is not None: + if transaction.response.type == defines.Types["CON"]: + self._start_retransmission(transaction, transaction.response) + self.send_datagram(transaction.response) + + elif isinstance(message, Message): + transaction = self._messageLayer.receive_empty(message) + if transaction is not None: + transaction = self._blockLayer.receive_empty(message, transaction) + self._observeLayer.receive_empty(message, transaction) + + else: # is Response + logger.error("Received response from %s", message.source) + + def send_datagram(self, message): + """ + Send a message through the udp socket. + + :type message: Message + :param message: the message to send + """ + if not self.stopped.isSet(): + host, port = message.destination + logger.debug("send_datagram - " + str(message)) + serializer = Serializer() + + message = serializer.serialize(message) + + self._socket.sendto(message, (host, port)) + + def _start_retransmission(self, transaction, message): + """ + Start the retransmission task. + + :type transaction: Transaction + :param transaction: the transaction that owns the message that needs retransmission + :type message: Message + :param message: the message that needs the retransmission task + """ + with transaction: + if message.type == defines.Types['CON']: + future_time = random.uniform(defines.ACK_TIMEOUT, (defines.ACK_TIMEOUT * defines.ACK_RANDOM_FACTOR)) + transaction.retransmit_thread = threading.Thread(target=self._retransmit, + args=(transaction, message, future_time, 0)) + transaction.retransmit_stop = threading.Event() + self.to_be_stopped.append(transaction.retransmit_stop) + transaction.retransmit_thread.start() + + def _retransmit(self, transaction, message, future_time, retransmit_count): + """ + Thread function to retransmit the message in the future + + :param transaction: the transaction that owns the message that needs retransmission + :param message: the message that needs the retransmission task + :param future_time: the amount of time to wait before a new attempt + :param retransmit_count: the number of retransmissions + """ + with transaction: + while retransmit_count < defines.MAX_RETRANSMIT and (not message.acknowledged and not message.rejected) \ + and not self.stopped.isSet(): + transaction.retransmit_stop.wait(timeout=future_time) + if not message.acknowledged and not message.rejected and not self.stopped.isSet(): + retransmit_count += 1 + future_time *= 2 + self.send_datagram(message) + + if message.acknowledged or message.rejected: + message.timeouted = False + else: + logger.warning("Give up on message {message}".format(message=message.line_print)) + message.timeouted = True + if message.observe is not None: + self._observeLayer.remove_subscriber(message) + + try: + self.to_be_stopped.remove(transaction.retransmit_stop) + except ValueError: + pass + transaction.retransmit_stop = None + transaction.retransmit_thread = None + + def _start_separate_timer(self, transaction): + """ + Start a thread to handle separate mode. + + :type transaction: Transaction + :param transaction: the transaction that is in processing + :rtype : the Timer object + """ + t = threading.Timer(defines.ACK_TIMEOUT, self._send_ack, (transaction,)) + t.start() + return t + + @staticmethod + def _stop_separate_timer(timer): + """ + Stop the separate Thread if an answer has been already provided to the client. + + :param timer: The Timer object + """ + timer.cancel() + + def _send_ack(self, transaction): + """ + Sends an ACK message for the request. + + :param transaction: the transaction that owns the request + """ + + ack = Message() + ack.type = defines.Types['ACK'] + + if not transaction.request.acknowledged: + ack = self._messageLayer.send_empty(transaction, transaction.request, ack) + self.send_datagram(ack) diff --git a/coapthon/http_proxy/__init__.py b/coapthon/http_proxy/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/coapthon/http_proxy/__init__.py @@ -0,0 +1 @@ + diff --git a/coapthon/http_proxy/http_coap_proxy.py b/coapthon/http_proxy/http_coap_proxy.py new file mode 100644 index 0000000..83b689c --- /dev/null +++ b/coapthon/http_proxy/http_coap_proxy.py @@ -0,0 +1,283 @@ +import argparse +import logging + +from http.server import BaseHTTPRequestHandler, HTTPServer +from coapthon.client.helperclient import HelperClient +from coapthon.utils import parse_uri +from coapthon.defines import Codes, DEFAULT_HC_PATH, HC_PROXY_DEFAULT_PORT, COAP_DEFAULT_PORT, LOCALHOST, BAD_REQUEST, \ + NOT_IMPLEMENTED, CoAP_HTTP +from coapthon.defines import COAP_PREFACE +from urllib.parse import urlparse + +__author__ = "Marco Ieni, Davide Foti" +__email__ = "marcoieni94@gmail.com, davidefoti.uni@gmail.com" + +logger = logging.getLogger(__name__) + +hc_path = DEFAULT_HC_PATH + +""" the class that realizes the HTTP-CoAP Proxy """ + + +class HCProxy: + """ + This program implements an HTTP-CoAP Proxy without using external libraries. + It is assumed that URI is formatted like this: + http://hc_proxy_ip:proxy_port/hc/coap://server_coap_ip:server_coap_port/resource + You can run this program passing the parameters from the command line or you can use the HCProxy class in your own + project. + """ + def __init__(self, path=DEFAULT_HC_PATH, hc_port=HC_PROXY_DEFAULT_PORT, ip=LOCALHOST, + coap_port=COAP_DEFAULT_PORT): + """ + Initialize the HC proxy. + + :param path: the path of the hc_proxy server + :param hc_port: the port of the hc_proxy server + :param ip: the ip of the hc_proxy server + :param coap_port: the coap server port you want to reach + """ + global hc_path + hc_path = HCProxy.get_formatted_path(path) + self.hc_port = hc_port + self.ip = ip + self.coap_port = coap_port + + def run(self): + """ + Start the proxy. + """ + server_address = (self.ip, self.hc_port) + hc_proxy = HTTPServer(server_address, HCProxyHandler) + logger.info('Starting HTTP-CoAP Proxy...') + hc_proxy.serve_forever() # the server listen to http://ip:hc_port/path + + @staticmethod + def get_formatted_path(path): + """ + Uniform the path string + + :param path: the path + :return: the uniform path + """ + if path[0] != '/': + path = '/' + path + if path[-1] != '/': + path = '{0}/'.format(path) + return path + + +class CoapUri: # this class takes the URI from the HTTP URI + """ Class that can manage and inbox the CoAP URI """ + def __init__(self, coap_uri): + self.uri = coap_uri + self.host, self.port, self.path = parse_uri(coap_uri) + + def get_uri_as_list(self): + """ + Split the uri into :///;?# + + :return: the split uri + """ + return urlparse(self.uri) + + def get_payload(self): + """ + Return the query string of the uri. + + :return: the query string as a list + """ + temp = self.get_uri_as_list() + query_string = temp[4] + if query_string == "": + return None # Bad request error code + query_string_as_list = str.split(query_string, "=") + return query_string_as_list[1] + + def __str__(self): + return self.uri + + +class HCProxyHandler(BaseHTTPRequestHandler): + """ It maps the requests from HTTP to CoAP """ + coap_uri = None + client = None + + def set_coap_uri(self): + """ + Create a CoAP Uri + """ + self.coap_uri = CoapUri(self.path[len(hc_path):]) + + def do_initial_operations(self): + """ + Setup the client for interact with remote server + """ + if not self.request_hc_path_corresponds(): + # the http URI of the request is not the same of the one specified by the admin for the hc proxy, + # so I do not answer + # For example the admin setup the http proxy URI like: "http://127.0.0.1:8080:/my_hc_path/" and the URI of + # the requests asks for "http://127.0.0.1:8080:/another_hc_path/" + return + self.set_coap_uri() + self.client = HelperClient(server=(self.coap_uri.host, self.coap_uri.port)) + + def do_GET(self): + """ + Perform a GET request + """ + self.do_initial_operations() + coap_response = self.client.get(self.coap_uri.path) + self.client.stop() + logger.info("Server response: %s", coap_response.pretty_print()) + self.set_http_response(coap_response) + + def do_HEAD(self): + """ + Perform a HEAD request + """ + self.do_initial_operations() + # the HEAD method is not present in CoAP, so we treat it + # like if it was a GET and then we exclude the body from the response + # with send_body=False we say that we do not need the body, because it is a HEAD request + coap_response = self.client.get(self.coap_uri.path) + self.client.stop() + logger.info("Server response: %s", coap_response.pretty_print()) + self.set_http_header(coap_response) + + def do_POST(self): + """ + Perform a POST request + """ + # Doesn't do anything with posted data + # print "uri: ", self.client_address, self.path + self.do_initial_operations() + payload = self.coap_uri.get_payload() + if payload is None: + logger.error("BAD POST REQUEST") + self.send_error(BAD_REQUEST) + return + coap_response = self.client.post(self.coap_uri.path, payload) + self.client.stop() + logger.info("Server response: %s", coap_response.pretty_print()) + self.set_http_response(coap_response) + + def do_PUT(self): + """ + Perform a PUT request + """ + self.do_initial_operations() + payload = self.coap_uri.get_payload() + if payload is None: + logger.error("BAD PUT REQUEST") + self.send_error(BAD_REQUEST) + return + logger.debug(payload) + coap_response = self.client.put(self.coap_uri.path, payload) + self.client.stop() + logger.debug("Server response: %s", coap_response.pretty_print()) + self.set_http_response(coap_response) + + def do_DELETE(self): + """ + Perform a DELETE request + """ + self.do_initial_operations() + coap_response = self.client.delete(self.coap_uri.path) + self.client.stop() + logger.debug("Server response: %s", coap_response.pretty_print()) + self.set_http_response(coap_response) + + def do_CONNECT(self): + """ + Perform a CONNECT request. Reply with error, not implemented in CoAP + """ + self.send_error(NOT_IMPLEMENTED) + + def do_OPTIONS(self): + """ + Perform a OPTIONS request. Reply with error, not implemented in CoAP + """ + self.send_error(NOT_IMPLEMENTED) + + def do_TRACE(self): + """ + Perform a TRACE request. Reply with error, not implemented in CoAP + """ + self.send_error(NOT_IMPLEMENTED) + + def request_hc_path_corresponds(self): + """ + Tells if the hc path of the request corresponds to that specified by the admin + + :return: a boolean that says if it corresponds or not + """ + uri_path = self.path.split(COAP_PREFACE) + request_hc_path = uri_path[0] + logger.debug("HCPATH: %s", hc_path) + # print HC_PATH + logger.debug("URI: %s", request_hc_path) + if hc_path != request_hc_path: + return False + else: + return True + + def set_http_header(self, coap_response): + """ + Sets http headers. + + :param coap_response: the coap response + """ + logger.debug( + ("Server: %s\n"\ + "codice risposta: %s\n"\ + "PROXED: %s\n"\ + "payload risposta: %s"), + coap_response.source, + coap_response.code, + CoAP_HTTP[Codes.LIST[coap_response.code].name], + coap_response.payload) + self.send_response(int(CoAP_HTTP[Codes.LIST[coap_response.code].name])) + self.send_header('Content-type', 'text/html') + self.end_headers() + + def set_http_body(self, coap_response): + """ + Set http body. + + :param coap_response: the coap response + """ + if coap_response.payload is not None: + body = "

", coap_response.payload, "

" + self.wfile.write("".join(body)) + else: + self.wfile.write("

None

") + + def set_http_response(self, coap_response): + """ + Set http response. + + :param coap_response: the coap response + """ + self.set_http_header(coap_response) + self.set_http_body(coap_response) + return + + +def get_command_line_args(): + parser = argparse.ArgumentParser(description='Run the HTTP-CoAP Proxy.') + parser.add_argument('-p', dest='path', default=DEFAULT_HC_PATH, + help='the path of the hc_proxy server') + parser.add_argument('-hp', dest='hc_port', default=HC_PROXY_DEFAULT_PORT, + help='the port of the hc_proxy server') + parser.add_argument('-ip', dest='ip', default=LOCALHOST, + help='the ip of the hc_proxy server') + parser.add_argument('-cp', dest='coap_port', default=COAP_DEFAULT_PORT, + help='the coap server port you want to reach') + return parser.parse_args() + + +if __name__ == "__main__": + args = get_command_line_args() + hc_proxy = HCProxy(args.path, int(args.hc_port), args.ip, args.coap_port) + hc_proxy.run() diff --git a/coapthon/http_proxy/http_coap_proxy.py.bak b/coapthon/http_proxy/http_coap_proxy.py.bak new file mode 100644 index 0000000..df07df6 --- /dev/null +++ b/coapthon/http_proxy/http_coap_proxy.py.bak @@ -0,0 +1,283 @@ +import argparse +import logging + +from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer +from coapthon.client.helperclient import HelperClient +from coapthon.utils import parse_uri +from coapthon.defines import Codes, DEFAULT_HC_PATH, HC_PROXY_DEFAULT_PORT, COAP_DEFAULT_PORT, LOCALHOST, BAD_REQUEST, \ + NOT_IMPLEMENTED, CoAP_HTTP +from coapthon.defines import COAP_PREFACE +from urlparse import urlparse + +__author__ = "Marco Ieni, Davide Foti" +__email__ = "marcoieni94@gmail.com, davidefoti.uni@gmail.com" + +logger = logging.getLogger(__name__) + +hc_path = DEFAULT_HC_PATH + +""" the class that realizes the HTTP-CoAP Proxy """ + + +class HCProxy: + """ + This program implements an HTTP-CoAP Proxy without using external libraries. + It is assumed that URI is formatted like this: + http://hc_proxy_ip:proxy_port/hc/coap://server_coap_ip:server_coap_port/resource + You can run this program passing the parameters from the command line or you can use the HCProxy class in your own + project. + """ + def __init__(self, path=DEFAULT_HC_PATH, hc_port=HC_PROXY_DEFAULT_PORT, ip=LOCALHOST, + coap_port=COAP_DEFAULT_PORT): + """ + Initialize the HC proxy. + + :param path: the path of the hc_proxy server + :param hc_port: the port of the hc_proxy server + :param ip: the ip of the hc_proxy server + :param coap_port: the coap server port you want to reach + """ + global hc_path + hc_path = HCProxy.get_formatted_path(path) + self.hc_port = hc_port + self.ip = ip + self.coap_port = coap_port + + def run(self): + """ + Start the proxy. + """ + server_address = (self.ip, self.hc_port) + hc_proxy = HTTPServer(server_address, HCProxyHandler) + logger.info('Starting HTTP-CoAP Proxy...') + hc_proxy.serve_forever() # the server listen to http://ip:hc_port/path + + @staticmethod + def get_formatted_path(path): + """ + Uniform the path string + + :param path: the path + :return: the uniform path + """ + if path[0] != '/': + path = '/' + path + if path[-1] != '/': + path = '{0}/'.format(path) + return path + + +class CoapUri: # this class takes the URI from the HTTP URI + """ Class that can manage and inbox the CoAP URI """ + def __init__(self, coap_uri): + self.uri = coap_uri + self.host, self.port, self.path = parse_uri(coap_uri) + + def get_uri_as_list(self): + """ + Split the uri into :///;?# + + :return: the split uri + """ + return urlparse(self.uri) + + def get_payload(self): + """ + Return the query string of the uri. + + :return: the query string as a list + """ + temp = self.get_uri_as_list() + query_string = temp[4] + if query_string == "": + return None # Bad request error code + query_string_as_list = str.split(query_string, "=") + return query_string_as_list[1] + + def __str__(self): + return self.uri + + +class HCProxyHandler(BaseHTTPRequestHandler): + """ It maps the requests from HTTP to CoAP """ + coap_uri = None + client = None + + def set_coap_uri(self): + """ + Create a CoAP Uri + """ + self.coap_uri = CoapUri(self.path[len(hc_path):]) + + def do_initial_operations(self): + """ + Setup the client for interact with remote server + """ + if not self.request_hc_path_corresponds(): + # the http URI of the request is not the same of the one specified by the admin for the hc proxy, + # so I do not answer + # For example the admin setup the http proxy URI like: "http://127.0.0.1:8080:/my_hc_path/" and the URI of + # the requests asks for "http://127.0.0.1:8080:/another_hc_path/" + return + self.set_coap_uri() + self.client = HelperClient(server=(self.coap_uri.host, self.coap_uri.port)) + + def do_GET(self): + """ + Perform a GET request + """ + self.do_initial_operations() + coap_response = self.client.get(self.coap_uri.path) + self.client.stop() + logger.info("Server response: %s", coap_response.pretty_print()) + self.set_http_response(coap_response) + + def do_HEAD(self): + """ + Perform a HEAD request + """ + self.do_initial_operations() + # the HEAD method is not present in CoAP, so we treat it + # like if it was a GET and then we exclude the body from the response + # with send_body=False we say that we do not need the body, because it is a HEAD request + coap_response = self.client.get(self.coap_uri.path) + self.client.stop() + logger.info("Server response: %s", coap_response.pretty_print()) + self.set_http_header(coap_response) + + def do_POST(self): + """ + Perform a POST request + """ + # Doesn't do anything with posted data + # print "uri: ", self.client_address, self.path + self.do_initial_operations() + payload = self.coap_uri.get_payload() + if payload is None: + logger.error("BAD POST REQUEST") + self.send_error(BAD_REQUEST) + return + coap_response = self.client.post(self.coap_uri.path, payload) + self.client.stop() + logger.info("Server response: %s", coap_response.pretty_print()) + self.set_http_response(coap_response) + + def do_PUT(self): + """ + Perform a PUT request + """ + self.do_initial_operations() + payload = self.coap_uri.get_payload() + if payload is None: + logger.error("BAD PUT REQUEST") + self.send_error(BAD_REQUEST) + return + logger.debug(payload) + coap_response = self.client.put(self.coap_uri.path, payload) + self.client.stop() + logger.debug("Server response: %s", coap_response.pretty_print()) + self.set_http_response(coap_response) + + def do_DELETE(self): + """ + Perform a DELETE request + """ + self.do_initial_operations() + coap_response = self.client.delete(self.coap_uri.path) + self.client.stop() + logger.debug("Server response: %s", coap_response.pretty_print()) + self.set_http_response(coap_response) + + def do_CONNECT(self): + """ + Perform a CONNECT request. Reply with error, not implemented in CoAP + """ + self.send_error(NOT_IMPLEMENTED) + + def do_OPTIONS(self): + """ + Perform a OPTIONS request. Reply with error, not implemented in CoAP + """ + self.send_error(NOT_IMPLEMENTED) + + def do_TRACE(self): + """ + Perform a TRACE request. Reply with error, not implemented in CoAP + """ + self.send_error(NOT_IMPLEMENTED) + + def request_hc_path_corresponds(self): + """ + Tells if the hc path of the request corresponds to that specified by the admin + + :return: a boolean that says if it corresponds or not + """ + uri_path = self.path.split(COAP_PREFACE) + request_hc_path = uri_path[0] + logger.debug("HCPATH: %s", hc_path) + # print HC_PATH + logger.debug("URI: %s", request_hc_path) + if hc_path != request_hc_path: + return False + else: + return True + + def set_http_header(self, coap_response): + """ + Sets http headers. + + :param coap_response: the coap response + """ + logger.debug( + ("Server: %s\n"\ + "codice risposta: %s\n"\ + "PROXED: %s\n"\ + "payload risposta: %s"), + coap_response.source, + coap_response.code, + CoAP_HTTP[Codes.LIST[coap_response.code].name], + coap_response.payload) + self.send_response(int(CoAP_HTTP[Codes.LIST[coap_response.code].name])) + self.send_header('Content-type', 'text/html') + self.end_headers() + + def set_http_body(self, coap_response): + """ + Set http body. + + :param coap_response: the coap response + """ + if coap_response.payload is not None: + body = "

", coap_response.payload, "

" + self.wfile.write("".join(body)) + else: + self.wfile.write("

None

") + + def set_http_response(self, coap_response): + """ + Set http response. + + :param coap_response: the coap response + """ + self.set_http_header(coap_response) + self.set_http_body(coap_response) + return + + +def get_command_line_args(): + parser = argparse.ArgumentParser(description='Run the HTTP-CoAP Proxy.') + parser.add_argument('-p', dest='path', default=DEFAULT_HC_PATH, + help='the path of the hc_proxy server') + parser.add_argument('-hp', dest='hc_port', default=HC_PROXY_DEFAULT_PORT, + help='the port of the hc_proxy server') + parser.add_argument('-ip', dest='ip', default=LOCALHOST, + help='the ip of the hc_proxy server') + parser.add_argument('-cp', dest='coap_port', default=COAP_DEFAULT_PORT, + help='the coap server port you want to reach') + return parser.parse_args() + + +if __name__ == "__main__": + args = get_command_line_args() + hc_proxy = HCProxy(args.path, int(args.hc_port), args.ip, args.coap_port) + hc_proxy.run() diff --git a/coapthon/layers/__init__.py b/coapthon/layers/__init__.py new file mode 100644 index 0000000..f89a37d --- /dev/null +++ b/coapthon/layers/__init__.py @@ -0,0 +1 @@ +__author__ = 'Giacomo Tanganelli' \ No newline at end of file diff --git a/coapthon/layers/blocklayer.py b/coapthon/layers/blocklayer.py new file mode 100644 index 0000000..451ba3c --- /dev/null +++ b/coapthon/layers/blocklayer.py @@ -0,0 +1,309 @@ +import logging +from coapthon import defines +from coapthon.messages.request import Request +from coapthon.messages.response import Response + +logger = logging.getLogger(__name__) + +__author__ = 'Giacomo Tanganelli' + + +class BlockItem(object): + def __init__(self, byte, num, m, size, payload=None, content_type=None): + """ + Data structure to store Block parameters + + :param byte: the last byte exchanged + :param num: the num field of the block option + :param m: the M bit of the block option + :param size: the size field of the block option + :param payload: the overall payload received in all blocks + :param content_type: the content-type of the payload + """ + self.byte = byte + self.num = num + self.m = m + self.size = size + self.payload = payload + self.content_type = content_type + + +class BlockLayer(object): + """ + Handle the Blockwise options. Hides all the exchange to both servers and clients. + """ + def __init__(self): + self._block1_sent = {} + self._block2_sent = {} + self._block1_receive = {} + self._block2_receive = {} + + def receive_request(self, transaction): + """ + Handles the Blocks option in a incoming request. + + :type transaction: Transaction + :param transaction: the transaction that owns the request + :rtype : Transaction + :return: the edited transaction + """ + if transaction.request.block2 is not None: + host, port = transaction.request.source + key_token = hash(str(host) + str(port) + str(transaction.request.token)) + num, m, size = transaction.request.block2 + if key_token in self._block2_receive: + self._block2_receive[key_token].num = num + self._block2_receive[key_token].size = size + self._block2_receive[key_token].m = m + del transaction.request.block2 + else: + # early negotiation + byte = 0 + self._block2_receive[key_token] = BlockItem(byte, num, m, size) + del transaction.request.block2 + + elif transaction.request.block1 is not None: + # POST or PUT + host, port = transaction.request.source + key_token = hash(str(host) + str(port) + str(transaction.request.token)) + num, m, size = transaction.request.block1 + if key_token in self._block1_receive: + content_type = transaction.request.content_type + if num != self._block1_receive[key_token].num \ + or content_type != self._block1_receive[key_token].content_type: + # Error Incomplete + return self.incomplete(transaction) + self._block1_receive[key_token].payload += transaction.request.payload + else: + # first block + if num != 0: + # Error Incomplete + return self.incomplete(transaction) + content_type = transaction.request.content_type + self._block1_receive[key_token] = BlockItem(size, num, m, size, transaction.request.payload, + content_type) + + if m == 0: + transaction.request.payload = self._block1_receive[key_token].payload + # end of blockwise + del transaction.request.block1 + transaction.block_transfer = False + del self._block1_receive[key_token] + return transaction + else: + # Continue + transaction.block_transfer = True + transaction.response = Response() + transaction.response.destination = transaction.request.source + transaction.response.token = transaction.request.token + transaction.response.code = defines.Codes.CONTINUE.number + transaction.response.block1 = (num, m, size) + + num += 1 + byte = size + self._block1_receive[key_token].byte = byte + self._block1_receive[key_token].num = num + self._block1_receive[key_token].size = size + self._block1_receive[key_token].m = m + + return transaction + + def receive_response(self, transaction): + """ + Handles the Blocks option in a incoming response. + + :type transaction: Transaction + :param transaction: the transaction that owns the response + :rtype : Transaction + :return: the edited transaction + """ + host, port = transaction.response.source + key_token = hash(str(host) + str(port) + str(transaction.response.token)) + if key_token in self._block1_sent and transaction.response.block1 is not None: + item = self._block1_sent[key_token] + transaction.block_transfer = True + if item.m == 0: + transaction.block_transfer = False + del transaction.request.block1 + return transaction + n_num, n_m, n_size = transaction.response.block1 + if n_num != item.num: # pragma: no cover + logger.warning("Blockwise num acknowledged error, expected " + str(item.num) + " received " + + str(n_num)) + return None + if n_size < item.size: + logger.debug("Scale down size, was " + str(item.size) + " become " + str(n_size)) + item.size = n_size + request = transaction.request + del request.mid + del request.block1 + request.payload = item.payload[item.byte: item.byte+item.size] + item.num += 1 + item.byte += item.size + if len(item.payload) <= item.byte: + m = 0 + else: + m = 1 + request.block1 = (item.num, m, item.size) + elif transaction.response.block2 is not None: + + num, m, size = transaction.response.block2 + if m == 1: + transaction.block_transfer = True + if key_token in self._block2_sent: + item = self._block2_sent[key_token] + if num != item.num: # pragma: no cover + logger.error("Receive unwanted block") + return self.error(transaction, defines.Codes.REQUEST_ENTITY_INCOMPLETE.number) + if item.content_type is None: + item.content_type = transaction.response.content_type + if item.content_type != transaction.response.content_type: # pragma: no cover + logger.error("Content-type Error") + return self.error(transaction, defines.Codes.UNSUPPORTED_CONTENT_FORMAT.number) + item.byte += size + item.num = num + 1 + item.size = size + item.m = m + item.payload += transaction.response.payload + else: + item = BlockItem(size, num + 1, m, size, transaction.response.payload, + transaction.response.content_type) + self._block2_sent[key_token] = item + request = transaction.request + del request.mid + del request.block2 + request.block2 = (item.num, 0, item.size) + else: + transaction.block_transfer = False + if key_token in self._block2_sent: + if self._block2_sent[key_token].content_type != transaction.response.content_type: # pragma: no cover + logger.error("Content-type Error") + return self.error(transaction, defines.Codes.UNSUPPORTED_CONTENT_FORMAT.number) + transaction.response.payload = self._block2_sent[key_token].payload + transaction.response.payload + del self._block2_sent[key_token] + else: + transaction.block_transfer = False + return transaction + + def receive_empty(self, empty, transaction): + """ + Dummy function. Used to do not broke the layered architecture. + + :type empty: Message + :param empty: the received empty message + :type transaction: Transaction + :param transaction: the transaction that owns the empty message + :rtype : Transaction + :return: the transaction + """ + return transaction + + def send_response(self, transaction): + """ + Handles the Blocks option in a outgoing response. + + :type transaction: Transaction + :param transaction: the transaction that owns the response + :rtype : Transaction + :return: the edited transaction + """ + host, port = transaction.request.source + key_token = hash(str(host) + str(port) + str(transaction.request.token)) + if (key_token in self._block2_receive and transaction.response.payload is not None) or \ + (transaction.response.payload is not None and len(transaction.response.payload) > defines.MAX_PAYLOAD): + if key_token in self._block2_receive: + + byte = self._block2_receive[key_token].byte + size = self._block2_receive[key_token].size + num = self._block2_receive[key_token].num + + else: + byte = 0 + num = 0 + size = defines.MAX_PAYLOAD + m = 1 + + self._block2_receive[key_token] = BlockItem(byte, num, m, size) + + if len(transaction.response.payload) > (byte + size): + m = 1 + else: + m = 0 + transaction.response.payload = transaction.response.payload[byte:byte + size] + del transaction.response.block2 + transaction.response.block2 = (num, m, size) + + self._block2_receive[key_token].byte += size + self._block2_receive[key_token].num += 1 + if m == 0: + del self._block2_receive[key_token] + + return transaction + + def send_request(self, request): + """ + Handles the Blocks option in a outgoing request. + + :type request: Request + :param request: the outgoing request + :return: the edited request + """ + assert isinstance(request, Request) + if request.block1 or (request.payload is not None and len(request.payload) > defines.MAX_PAYLOAD): + host, port = request.destination + key_token = hash(str(host) + str(port) + str(request.token)) + if request.block1: + num, m, size = request.block1 + else: + num = 0 + m = 1 + size = defines.MAX_PAYLOAD + + self._block1_sent[key_token] = BlockItem(size, num, m, size, request.payload, request.content_type) + request.payload = request.payload[0:size] + del request.block1 + request.block1 = (num, m, size) + elif request.block2: + host, port = request.destination + key_token = hash(str(host) + str(port) + str(request.token)) + num, m, size = request.block2 + item = BlockItem(size, num, m, size, "", None) + self._block2_sent[key_token] = item + return request + return request + + @staticmethod + def incomplete(transaction): + """ + Notifies incomplete blockwise exchange. + + :type transaction: Transaction + :param transaction: the transaction that owns the response + :rtype : Transaction + :return: the edited transaction + """ + transaction.block_transfer = True + transaction.response = Response() + transaction.response.destination = transaction.request.source + transaction.response.token = transaction.request.token + transaction.response.code = defines.Codes.REQUEST_ENTITY_INCOMPLETE.number + return transaction + + @staticmethod + def error(transaction, code): # pragma: no cover + """ + Notifies generic error on blockwise exchange. + + :type transaction: Transaction + :param transaction: the transaction that owns the response + :rtype : Transaction + :return: the edited transaction + """ + transaction.block_transfer = True + transaction.response = Response() + transaction.response.destination = transaction.request.source + transaction.response.type = defines.Types["RST"] + transaction.response.token = transaction.request.token + transaction.response.code = code + return transaction + diff --git a/coapthon/layers/cachelayer.py b/coapthon/layers/cachelayer.py new file mode 100644 index 0000000..24532f3 --- /dev/null +++ b/coapthon/layers/cachelayer.py @@ -0,0 +1,111 @@ +import logging + +from coapthon.defines import Codes + +from coapthon.caching.cache import * + +__author__ = 'Emilio Vallati' + +logger = logging.getLogger(__name__) + + +class CacheLayer(object): + + def __init__(self, mode, max_dim=2048): + """ + + :param max_dim: + """ + self.cache = Cache(mode, max_dim) + + def receive_request(self, transaction): + """ + checks the cache for a response to the request + + :param transaction: + :return: + """ + + transaction.cached_element = self.cache.search_response(transaction.request) + if transaction.cached_element is None: + transaction.cacheHit = False + else: + transaction.response = transaction.cached_element.cached_response + transaction.response.mid = transaction.request.mid + transaction.cacheHit = True + + age = transaction.cached_element.creation_time + transaction.cached_element.max_age - time.time() + if transaction.cached_element.freshness is True: + if age <= 0: + logger.debug("resource not fresh") + """ + if the resource is not fresh, its Etag must be added to the request so that the server might validate it instead of sending a new one + """ + transaction.cached_element.freshness = False + """ + ensuring that the request goes to the server + """ + transaction.cacheHit = False + logger.debug("requesting etag %s", transaction.response.etag) + transaction.request.etag = transaction.response.etag + else: + transaction.response.max_age = age + else: + transaction.cacheHit = False + return transaction + + def send_response(self, transaction): + """ + updates the cache with the response if there was a cache miss + + :param transaction: + :return: + """ + if transaction.cacheHit is False: + """ + handling response based on the code + """ + logger.debug("handling response") + self._handle_response(transaction) + return transaction + + def _handle_response(self, transaction): + """ + handles responses based on their type + + :param transaction: + :return: + """ + code = transaction.response.code + utils.check_code(code) + """ + VALID response: + change the current cache value by switching the option set with the one provided + also resets the timestamp + if the request etag is different from the response, send the cached response + """ + if code == Codes.VALID.number: + logger.debug("received VALID") + self.cache.validate(transaction.request, transaction.response) + if transaction.request.etag != transaction.response.etag: + element = self.cache.search_response(transaction.request) + transaction.response = element.cached_response + return transaction + + """ + CHANGED, CREATED or DELETED response: + mark the requested resource as not fresh + """ + if code == Codes.CHANGED.number or code == Codes.CREATED.number or code == Codes.DELETED.number: + target = self.cache.search_related(transaction.request) + if target is not None: + for element in target: + self.cache.mark(element) + return transaction + + """ + any other response code can be cached normally + """ + self.cache.cache_add(transaction.request, transaction.response) + return transaction + diff --git a/coapthon/layers/forwardLayer.py b/coapthon/layers/forwardLayer.py new file mode 100644 index 0000000..e36f68d --- /dev/null +++ b/coapthon/layers/forwardLayer.py @@ -0,0 +1,167 @@ +import copy +from coapthon.messages.request import Request +from coapclient import HelperClient +from coapthon.messages.response import Response +from coapthon import defines +from coapthon.resources.remoteResource import RemoteResource +from coapthon.utils import parse_uri + +__author__ = 'Giacomo Tanganelli' + + +class ForwardLayer(object): + """ + Class used by Proxies to forward messages. + """ + def __init__(self, server): + self._server = server + + def receive_request(self, transaction): + """ + Setup the transaction for forwarding purposes on Forward Proxies. + + :type transaction: Transaction + :param transaction: the transaction that owns the request + :rtype : Transaction + :return: the edited transaction + """ + uri = transaction.request.proxy_uri + if uri is None: + transaction.response = Response() + transaction.response.destination = transaction.request.source + transaction.response.token = transaction.request.token + transaction.response.type = defines.Types["RST"] + transaction.response.code = defines.Codes.BAD_REQUEST.number + return transaction + + host, port, path = parse_uri(uri) + path = str("/" + path) + transaction.response = Response() + transaction.response.destination = transaction.request.source + transaction.response.token = transaction.request.token + return self._forward_request(transaction, (host, port), path) + + def receive_request_reverse(self, transaction): + """ + Setup the transaction for forwarding purposes on Reverse Proxies. + + :type transaction: Transaction + :param transaction: the transaction that owns the request + :rtype : Transaction + :return: the edited transaction + """ + path = str("/" + transaction.request.uri_path) + transaction.response = Response() + transaction.response.destination = transaction.request.source + transaction.response.token = transaction.request.token + if path == defines.DISCOVERY_URL: + transaction = self._server.resourceLayer.discover(transaction) + else: + new = False + if transaction.request.code == defines.Codes.POST.number: + new_paths = self._server.root.with_prefix(path) + new_path = "/" + for tmp in new_paths: + if len(tmp) > len(new_path): + new_path = tmp + if path != new_path: + new = True + path = new_path + try: + resource = self._server.root[path] + except KeyError: + resource = None + if resource is None or path == '/': + # Not Found + transaction.response.code = defines.Codes.NOT_FOUND.number + else: + transaction.resource = resource + transaction = self._handle_request(transaction, new) + return transaction + + @staticmethod + def _forward_request(transaction, destination, path): + """ + Forward requests. + + :type transaction: Transaction + :param transaction: the transaction that owns the request + :param destination: the destination of the request (IP, port) + :param path: the path of the request. + :rtype : Transaction + :return: the edited transaction + """ + client = HelperClient(destination) + request = Request() + request.options = copy.deepcopy(transaction.request.options) + del request.block2 + del request.block1 + del request.uri_path + del request.proxy_uri + del request.proxy_schema + # TODO handle observing + del request.observe + # request.observe = transaction.request.observe + + request.uri_path = path + request.destination = destination + request.payload = transaction.request.payload + request.code = transaction.request.code + response = client.send_request(request) + client.stop() + if response is not None: + transaction.response.payload = response.payload + transaction.response.code = response.code + transaction.response.options = response.options + else: + transaction.response.code = defines.Codes.SERVICE_UNAVAILABLE.number + + return transaction + + def _handle_request(self, transaction, new_resource): + """ + Forward requests. Used by reverse proxies to also create new virtual resources on the proxy + in case of created resources + + :type new_resource: bool + :type transaction: Transaction + :param transaction: the transaction that owns the request + :rtype : Transaction + :param new_resource: if the request will generate a new resource + :return: the edited transaction + """ + client = HelperClient(transaction.resource.remote_server) + request = Request() + request.options = copy.deepcopy(transaction.request.options) + del request.block2 + del request.block1 + del request.uri_path + del request.proxy_uri + del request.proxy_schema + # TODO handle observing + del request.observe + # request.observe = transaction.request.observe + + request.uri_path = "/".join(transaction.request.uri_path.split("/")[1:]) + request.destination = transaction.resource.remote_server + request.payload = transaction.request.payload + request.code = transaction.request.code + response = client.send_request(request) + client.stop() + transaction.response.payload = response.payload + transaction.response.code = response.code + transaction.response.options = response.options + if response.code == defines.Codes.CREATED.number: + lp = transaction.response.location_path + del transaction.response.location_path + transaction.response.location_path = transaction.request.uri_path.split("/")[0] + "/" + lp + # TODO handle observing + if new_resource: + resource = RemoteResource('server', transaction.resource.remote_server, lp, coap_server=self, + visible=True, + observable=False, + allow_children=True) + self._server.add_resource(transaction.response.location_path, resource) + if response.code == defines.Codes.DELETED.number: + del self._server.root["/" + transaction.request.uri_path] + return transaction diff --git a/coapthon/layers/messagelayer.py b/coapthon/layers/messagelayer.py new file mode 100644 index 0000000..f4cba32 --- /dev/null +++ b/coapthon/layers/messagelayer.py @@ -0,0 +1,318 @@ +import logging +import random +import time +from coapthon.messages.message import Message +from coapthon import defines +from coapthon.messages.request import Request +from coapthon.transaction import Transaction + +__author__ = 'Giacomo Tanganelli' + +logger = logging.getLogger(__name__) + + +def str_append_hash(*args): + """ Convert each argument to a lower case string, appended, then hash """ + ret_hash = "" + for i in args: + ret_hash += str(i).lower() + + return hash(ret_hash) + + +class MessageLayer(object): + """ + Handles matching between messages (Message ID) and request/response (Token) + """ + def __init__(self, starting_mid): + """ + Set the layer internal structure. + + :param starting_mid: the first mid used to send messages. + """ + self._transactions = {} + self._transactions_token = {} + if starting_mid is not None: + self._current_mid = starting_mid + else: + self._current_mid = random.randint(1, 1000) + + def fetch_mid(self): + """ + Gets the next valid MID. + + :return: the mid to use + """ + current_mid = self._current_mid + self._current_mid += 1 + self._current_mid %= 65535 + return current_mid + + def purge(self): + for k in list(self._transactions.keys()): + now = time.time() + transaction = self._transactions[k] + if transaction.timestamp + defines.EXCHANGE_LIFETIME < now: + logger.debug("Delete transaction") + del self._transactions[k] + for k in list(self._transactions_token.keys()): + now = time.time() + transaction = self._transactions_token[k] + if transaction.timestamp + defines.EXCHANGE_LIFETIME < now: + logger.debug("Delete transaction") + del self._transactions_token[k] + + def receive_request(self, request): + """ + Handle duplicates and store received messages. + + :type request: Request + :param request: the incoming request + :rtype : Transaction + :return: the edited transaction + """ + logger.debug("receive_request - " + str(request)) + try: + host, port = request.source + except AttributeError: + return + key_mid = str_append_hash(host, port, request.mid) + key_token = str_append_hash(host, port, request.token) + + if key_mid in list(self._transactions.keys()): + # Duplicated + self._transactions[key_mid].request.duplicated = True + transaction = self._transactions[key_mid] + else: + request.timestamp = time.time() + transaction = Transaction(request=request, timestamp=request.timestamp) + with transaction: + self._transactions[key_mid] = transaction + self._transactions_token[key_token] = transaction + return transaction + + def receive_response(self, response): + """ + Pair responses with requests. + + :type response: Response + :param response: the received response + :rtype : Transaction + :return: the transaction to which the response belongs to + """ + logger.debug("receive_response - " + str(response)) + try: + host, port = response.source + except AttributeError: + return + key_mid = str_append_hash(host, port, response.mid) + key_mid_multicast = str_append_hash(defines.ALL_COAP_NODES, port, response.mid) + key_token = str_append_hash(host, port, response.token) + key_token_multicast = str_append_hash(defines.ALL_COAP_NODES, port, response.token) + if key_mid in list(self._transactions.keys()): + transaction = self._transactions[key_mid] + if response.token != transaction.request.token: + logger.warning("Tokens does not match - response message " + str(host) + ":" + str(port)) + return None, False + elif key_token in self._transactions_token: + transaction = self._transactions_token[key_token] + elif key_mid_multicast in list(self._transactions.keys()): + transaction = self._transactions[key_mid_multicast] + elif key_token_multicast in self._transactions_token: + transaction = self._transactions_token[key_token_multicast] + if response.token != transaction.request.token: + logger.warning("Tokens does not match - response message " + str(host) + ":" + str(port)) + return None, False + else: + logger.warning("Un-Matched incoming response message " + str(host) + ":" + str(port)) + return None, False + send_ack = False + if response.type == defines.Types["CON"]: + send_ack = True + + transaction.request.acknowledged = True + transaction.completed = True + transaction.response = response + if transaction.retransmit_stop is not None: + transaction.retransmit_stop.set() + return transaction, send_ack + + def receive_empty(self, message): + """ + Pair ACKs with requests. + + :type message: Message + :param message: the received message + :rtype : Transaction + :return: the transaction to which the message belongs to + """ + logger.debug("receive_empty - " + str(message)) + try: + host, port = message.source + except AttributeError: + return + key_mid = str_append_hash(host, port, message.mid) + key_mid_multicast = str_append_hash(defines.ALL_COAP_NODES, port, message.mid) + key_token = str_append_hash(host, port, message.token) + key_token_multicast = str_append_hash(defines.ALL_COAP_NODES, port, message.token) + if key_mid in list(self._transactions.keys()): + transaction = self._transactions[key_mid] + elif key_token in self._transactions_token: + transaction = self._transactions_token[key_token] + elif key_mid_multicast in list(self._transactions.keys()): + transaction = self._transactions[key_mid_multicast] + elif key_token_multicast in self._transactions_token: + transaction = self._transactions_token[key_token_multicast] + else: + logger.warning("Un-Matched incoming empty message " + str(host) + ":" + str(port)) + return None + + if message.type == defines.Types["ACK"]: + if not transaction.request.acknowledged: + transaction.request.acknowledged = True + elif (transaction.response is not None) and (not transaction.response.acknowledged): + transaction.response.acknowledged = True + elif message.type == defines.Types["RST"]: + if not transaction.request.acknowledged: + transaction.request.rejected = True + elif not transaction.response.acknowledged: + transaction.response.rejected = True + elif message.type == defines.Types["CON"]: + #implicit ACK (might have been lost) + logger.debug("Implicit ACK on received CON for waiting transaction") + transaction.request.acknowledged = True + else: + logger.warning("Unhandled message type...") + + if transaction.retransmit_stop is not None: + transaction.retransmit_stop.set() + + return transaction + + def send_request(self, request): + """ + Create the transaction and fill it with the outgoing request. + + :type request: Request + :param request: the request to send + :rtype : Transaction + :return: the created transaction + """ + logger.debug("send_request - " + str(request)) + assert isinstance(request, Request) + try: + host, port = request.destination + except AttributeError: + return + + request.timestamp = time.time() + transaction = Transaction(request=request, timestamp=request.timestamp) + if transaction.request.type is None: + transaction.request.type = defines.Types["CON"] + + if transaction.request.mid is None: + transaction.request.mid = self.fetch_mid() + + key_mid = str_append_hash(host, port, request.mid) + self._transactions[key_mid] = transaction + + key_token = str_append_hash(host, port, request.token) + self._transactions_token[key_token] = transaction + + return self._transactions[key_mid] + + def send_response(self, transaction): + """ + Set the type, the token and eventually the MID for the outgoing response + + :type transaction: Transaction + :param transaction: the transaction that owns the response + :rtype : Transaction + :return: the edited transaction + """ + logger.debug("send_response - " + str(transaction.response)) + if transaction.response.type is None: + if transaction.request.type == defines.Types["CON"] and not transaction.request.acknowledged: + transaction.response.type = defines.Types["ACK"] + transaction.response.mid = transaction.request.mid + transaction.response.acknowledged = True + transaction.completed = True + elif transaction.request.type == defines.Types["NON"]: + transaction.response.type = defines.Types["NON"] + else: + transaction.response.type = defines.Types["CON"] + transaction.response.token = transaction.request.token + + if transaction.response.mid is None: + transaction.response.mid = self.fetch_mid() + try: + host, port = transaction.response.destination + except AttributeError: + return + key_mid = str_append_hash(host, port, transaction.response.mid) + self._transactions[key_mid] = transaction + + transaction.request.acknowledged = True + return transaction + + def send_empty(self, transaction, related, message): + """ + Manage ACK or RST related to a transaction. Sets if the transaction has been acknowledged or rejected. + + :param transaction: the transaction + :param related: if the ACK/RST message is related to the request or the response. Must be equal to + transaction.request or to transaction.response or None + :type message: Message + :param message: the ACK or RST message to send + """ + logger.debug("send_empty - " + str(message)) + if transaction is None: + try: + host, port = message.destination + except AttributeError: + return + key_mid = str_append_hash(host, port, message.mid) + key_token = str_append_hash(host, port, message.token) + if key_mid in self._transactions: + transaction = self._transactions[key_mid] + related = transaction.response + elif key_token in self._transactions_token: + transaction = self._transactions_token[key_token] + related = transaction.response + else: + return message + + if message.type == defines.Types["ACK"]: + if transaction.request == related: + transaction.request.acknowledged = True + transaction.completed = True + message.mid = transaction.request.mid + message.code = 0 + message.destination = transaction.request.source + elif transaction.response == related: + transaction.response.acknowledged = True + transaction.completed = True + message.mid = transaction.response.mid + message.code = 0 + message.token = transaction.response.token + message.destination = transaction.response.source + + elif message.type == defines.Types["RST"]: + if transaction.request == related: + transaction.request.rejected = True + message._mid = transaction.request.mid + if message.mid is None: + message.mid = self.fetch_mid() + message.code = 0 + message.token = transaction.request.token + message.destination = transaction.request.source + elif transaction.response == related: + transaction.response.rejected = True + transaction.completed = True + message._mid = transaction.response.mid + if message.mid is None: + message.mid = self.fetch_mid() + message.code = 0 + message.token = transaction.response.token + message.destination = transaction.response.source + return message diff --git a/coapthon/layers/messagelayer.py.bak b/coapthon/layers/messagelayer.py.bak new file mode 100644 index 0000000..6c75695 --- /dev/null +++ b/coapthon/layers/messagelayer.py.bak @@ -0,0 +1,314 @@ +import logging +import random +import time +from coapthon.messages.message import Message +from coapthon import defines +from coapthon.messages.request import Request +from coapthon.transaction import Transaction + +__author__ = 'Giacomo Tanganelli' + +logger = logging.getLogger(__name__) + + +def str_append_hash(*args): + """ Convert each argument to a lower case string, appended, then hash """ + ret_hash = "" + for i in args: + ret_hash += str(i).lower() + + return hash(ret_hash) + + +class MessageLayer(object): + """ + Handles matching between messages (Message ID) and request/response (Token) + """ + def __init__(self, starting_mid): + """ + Set the layer internal structure. + + :param starting_mid: the first mid used to send messages. + """ + self._transactions = {} + self._transactions_token = {} + if starting_mid is not None: + self._current_mid = starting_mid + else: + self._current_mid = random.randint(1, 1000) + + def fetch_mid(self): + """ + Gets the next valid MID. + + :return: the mid to use + """ + current_mid = self._current_mid + self._current_mid += 1 + self._current_mid %= 65535 + return current_mid + + def purge(self): + for k in self._transactions.keys(): + now = time.time() + transaction = self._transactions[k] + if transaction.timestamp + defines.EXCHANGE_LIFETIME < now: + logger.debug("Delete transaction") + del self._transactions[k] + for k in self._transactions_token.keys(): + now = time.time() + transaction = self._transactions_token[k] + if transaction.timestamp + defines.EXCHANGE_LIFETIME < now: + logger.debug("Delete transaction") + del self._transactions_token[k] + + def receive_request(self, request): + """ + Handle duplicates and store received messages. + + :type request: Request + :param request: the incoming request + :rtype : Transaction + :return: the edited transaction + """ + logger.debug("receive_request - " + str(request)) + try: + host, port = request.source + except AttributeError: + return + key_mid = str_append_hash(host, port, request.mid) + key_token = str_append_hash(host, port, request.token) + + if key_mid in self._transactions.keys(): + # Duplicated + self._transactions[key_mid].request.duplicated = True + transaction = self._transactions[key_mid] + else: + request.timestamp = time.time() + transaction = Transaction(request=request, timestamp=request.timestamp) + with transaction: + self._transactions[key_mid] = transaction + self._transactions_token[key_token] = transaction + return transaction + + def receive_response(self, response): + """ + Pair responses with requests. + + :type response: Response + :param response: the received response + :rtype : Transaction + :return: the transaction to which the response belongs to + """ + logger.debug("receive_response - " + str(response)) + try: + host, port = response.source + except AttributeError: + return + key_mid = str_append_hash(host, port, response.mid) + key_mid_multicast = str_append_hash(defines.ALL_COAP_NODES, port, response.mid) + key_token = str_append_hash(host, port, response.token) + key_token_multicast = str_append_hash(defines.ALL_COAP_NODES, port, response.token) + if key_mid in self._transactions.keys(): + transaction = self._transactions[key_mid] + if response.token != transaction.request.token: + logger.warning("Tokens does not match - response message " + str(host) + ":" + str(port)) + return None, False + elif key_token in self._transactions_token: + transaction = self._transactions_token[key_token] + elif key_mid_multicast in self._transactions.keys(): + transaction = self._transactions[key_mid_multicast] + elif key_token_multicast in self._transactions_token: + transaction = self._transactions_token[key_token_multicast] + if response.token != transaction.request.token: + logger.warning("Tokens does not match - response message " + str(host) + ":" + str(port)) + return None, False + else: + logger.warning("Un-Matched incoming response message " + str(host) + ":" + str(port)) + return None, False + send_ack = False + if response.type == defines.Types["CON"]: + send_ack = True + + transaction.request.acknowledged = True + transaction.completed = True + transaction.response = response + if transaction.retransmit_stop is not None: + transaction.retransmit_stop.set() + return transaction, send_ack + + def receive_empty(self, message): + """ + Pair ACKs with requests. + + :type message: Message + :param message: the received message + :rtype : Transaction + :return: the transaction to which the message belongs to + """ + logger.debug("receive_empty - " + str(message)) + try: + host, port = message.source + except AttributeError: + return + key_mid = str_append_hash(host, port, message.mid) + key_mid_multicast = str_append_hash(defines.ALL_COAP_NODES, port, message.mid) + key_token = str_append_hash(host, port, message.token) + key_token_multicast = str_append_hash(defines.ALL_COAP_NODES, port, message.token) + if key_mid in self._transactions.keys(): + transaction = self._transactions[key_mid] + elif key_token in self._transactions_token: + transaction = self._transactions_token[key_token] + elif key_mid_multicast in self._transactions.keys(): + transaction = self._transactions[key_mid_multicast] + elif key_token_multicast in self._transactions_token: + transaction = self._transactions_token[key_token_multicast] + else: + logger.warning("Un-Matched incoming empty message " + str(host) + ":" + str(port)) + return None + + if message.type == defines.Types["ACK"]: + if not transaction.request.acknowledged: + transaction.request.acknowledged = True + elif (transaction.response is not None) and (not transaction.response.acknowledged): + transaction.response.acknowledged = True + elif message.type == defines.Types["RST"]: + if not transaction.request.acknowledged: + transaction.request.rejected = True + elif not transaction.response.acknowledged: + transaction.response.rejected = True + elif message.type == defines.Types["CON"]: + #implicit ACK (might have been lost) + logger.debug("Implicit ACK on received CON for waiting transaction") + transaction.request.acknowledged = True + else: + logger.warning("Unhandled message type...") + + if transaction.retransmit_stop is not None: + transaction.retransmit_stop.set() + + return transaction + + def send_request(self, request): + """ + Create the transaction and fill it with the outgoing request. + + :type request: Request + :param request: the request to send + :rtype : Transaction + :return: the created transaction + """ + logger.debug("send_request - " + str(request)) + assert isinstance(request, Request) + try: + host, port = request.destination + except AttributeError: + return + + request.timestamp = time.time() + transaction = Transaction(request=request, timestamp=request.timestamp) + if transaction.request.type is None: + transaction.request.type = defines.Types["CON"] + + if transaction.request.mid is None: + transaction.request.mid = self.fetch_mid() + + key_mid = str_append_hash(host, port, request.mid) + self._transactions[key_mid] = transaction + + key_token = str_append_hash(host, port, request.token) + self._transactions_token[key_token] = transaction + + return self._transactions[key_mid] + + def send_response(self, transaction): + """ + Set the type, the token and eventually the MID for the outgoing response + + :type transaction: Transaction + :param transaction: the transaction that owns the response + :rtype : Transaction + :return: the edited transaction + """ + logger.debug("send_response - " + str(transaction.response)) + if transaction.response.type is None: + if transaction.request.type == defines.Types["CON"] and not transaction.request.acknowledged: + transaction.response.type = defines.Types["ACK"] + transaction.response.mid = transaction.request.mid + transaction.response.acknowledged = True + transaction.completed = True + elif transaction.request.type == defines.Types["NON"]: + transaction.response.type = defines.Types["NON"] + else: + transaction.response.type = defines.Types["CON"] + transaction.response.token = transaction.request.token + + if transaction.response.mid is None: + transaction.response.mid = self.fetch_mid() + try: + host, port = transaction.response.destination + except AttributeError: + return + key_mid = str_append_hash(host, port, transaction.response.mid) + self._transactions[key_mid] = transaction + + transaction.request.acknowledged = True + return transaction + + def send_empty(self, transaction, related, message): + """ + Manage ACK or RST related to a transaction. Sets if the transaction has been acknowledged or rejected. + + :param transaction: the transaction + :param related: if the ACK/RST message is related to the request or the response. Must be equal to + transaction.request or to transaction.response or None + :type message: Message + :param message: the ACK or RST message to send + """ + logger.debug("send_empty - " + str(message)) + if transaction is None: + try: + host, port = message.destination + except AttributeError: + return + key_mid = str_append_hash(host, port, message.mid) + key_token = str_append_hash(host, port, message.token) + if key_mid in self._transactions: + transaction = self._transactions[key_mid] + related = transaction.response + elif key_token in self._transactions_token: + transaction = self._transactions_token[key_token] + related = transaction.response + else: + return message + + if message.type == defines.Types["ACK"]: + if transaction.request == related: + transaction.request.acknowledged = True + transaction.completed = True + message.mid = transaction.request.mid + message.code = 0 + message.destination = transaction.request.source + elif transaction.response == related: + transaction.response.acknowledged = True + transaction.completed = True + message.mid = transaction.response.mid + message.code = 0 + message.token = transaction.response.token + message.destination = transaction.response.source + + elif message.type == defines.Types["RST"]: + if transaction.request == related: + transaction.request.rejected = True + message._mid = transaction.request.mid + message.code = 0 + message.token = transaction.request.token + message.destination = transaction.request.source + elif transaction.response == related: + transaction.response.rejected = True + transaction.completed = True + message._mid = transaction.response.mid + message.code = 0 + message.token = transaction.response.token + message.destination = transaction.response.source + return message diff --git a/coapthon/layers/observelayer.py b/coapthon/layers/observelayer.py new file mode 100644 index 0000000..ae9f3fd --- /dev/null +++ b/coapthon/layers/observelayer.py @@ -0,0 +1,199 @@ +import logging +import time +from coapthon import defines + +__author__ = 'Giacomo Tanganelli' + +logger = logging.getLogger(__name__) + + +class ObserveItem(object): + def __init__(self, timestamp, non_counter, allowed, transaction): + """ + Data structure for the Observe option + + :param timestamp: the timestamop of last message sent + :param non_counter: the number of NON notification sent + :param allowed: if the client is allowed as observer + :param transaction: the transaction + """ + self.timestamp = timestamp + self.non_counter = non_counter + self.allowed = allowed + self.transaction = transaction + + +class ObserveLayer(object): + """ + Manage the observing feature. It store observing relationships. + """ + def __init__(self): + self._relations = {} + + def send_request(self, request): + """ + Add itself to the observing list + + :param request: the request + :return: the request unmodified + """ + if request.observe == 0: + # Observe request + host, port = request.destination + key_token = hash(str(host) + str(port) + str(request.token)) + + self._relations[key_token] = ObserveItem(time.time(), None, True, None) + + return request + + def receive_response(self, transaction): + """ + Sets notification's parameters. + + :type transaction: Transaction + :param transaction: the transaction + :rtype : Transaction + :return: the modified transaction + """ + host, port = transaction.response.source + key_token = hash(str(host) + str(port) + str(transaction.response.token)) + if key_token in self._relations and transaction.response.type == defines.Types["CON"]: + transaction.notification = True + return transaction + + def send_empty(self, message): + """ + Eventually remove from the observer list in case of a RST message. + + :type message: Message + :param message: the message + :return: the message unmodified + """ + host, port = message.destination + key_token = hash(str(host) + str(port) + str(message.token)) + if key_token in self._relations and message.type == defines.Types["RST"]: + del self._relations[key_token] + return message + + def receive_request(self, transaction): + """ + Manage the observe option in the request end eventually initialize the client for adding to + the list of observers or remove from the list. + + :type transaction: Transaction + :param transaction: the transaction that owns the request + :rtype : Transaction + :return: the modified transaction + """ + if transaction.request.observe == 0: + # Observe request + host, port = transaction.request.source + key_token = hash(str(host) + str(port) + str(transaction.request.token)) + non_counter = 0 + if key_token in self._relations: + # Renew registration + allowed = True + else: + allowed = False + self._relations[key_token] = ObserveItem(time.time(), non_counter, allowed, transaction) + elif transaction.request.observe == 1: + host, port = transaction.request.source + key_token = hash(str(host) + str(port) + str(transaction.request.token)) + logger.info("Remove Subscriber") + try: + del self._relations[key_token] + except KeyError: + pass + + return transaction + + def receive_empty(self, empty, transaction): + """ + Manage the observe feature to remove a client in case of a RST message receveide in reply to a notification. + + :type empty: Message + :param empty: the received message + :type transaction: Transaction + :param transaction: the transaction that owns the notification message + :rtype : Transaction + :return: the modified transaction + """ + if empty.type == defines.Types["RST"]: + host, port = transaction.request.source + key_token = hash(str(host) + str(port) + str(transaction.request.token)) + logger.info("Remove Subscriber") + try: + del self._relations[key_token] + except KeyError: + pass + transaction.completed = True + return transaction + + def send_response(self, transaction): + """ + Finalize to add the client to the list of observer. + + :type transaction: Transaction + :param transaction: the transaction that owns the response + :return: the transaction unmodified + """ + host, port = transaction.request.source + key_token = hash(str(host) + str(port) + str(transaction.request.token)) + if key_token in self._relations: + if transaction.response.code == defines.Codes.CONTENT.number: + if transaction.resource is not None and transaction.resource.observable: + + transaction.response.observe = transaction.resource.observe_count + self._relations[key_token].allowed = True + self._relations[key_token].transaction = transaction + self._relations[key_token].timestamp = time.time() + else: + del self._relations[key_token] + elif transaction.response.code >= defines.Codes.ERROR_LOWER_BOUND: + del self._relations[key_token] + return transaction + + def notify(self, resource, root=None): + """ + Prepare notification for the resource to all interested observers. + + :rtype: list + :param resource: the resource for which send a new notification + :param root: deprecated + :return: the list of transactions to be notified + """ + ret = [] + if root is not None: + resource_list = root.with_prefix_resource(resource.path) + else: + resource_list = [resource] + for key in list(self._relations.keys()): + if self._relations[key].transaction.resource in resource_list: + if self._relations[key].non_counter > defines.MAX_NON_NOTIFICATIONS \ + or self._relations[key].transaction.request.type == defines.Types["CON"]: + self._relations[key].transaction.response.type = defines.Types["CON"] + self._relations[key].non_counter = 0 + elif self._relations[key].transaction.request.type == defines.Types["NON"]: + self._relations[key].non_counter += 1 + self._relations[key].transaction.response.type = defines.Types["NON"] + self._relations[key].transaction.resource = resource + del self._relations[key].transaction.response.mid + del self._relations[key].transaction.response.token + ret.append(self._relations[key].transaction) + return ret + + def remove_subscriber(self, message): + """ + Remove a subscriber based on token. + + :param message: the message + """ + logger.debug("Remove Subcriber") + host, port = message.destination + key_token = hash(str(host) + str(port) + str(message.token)) + try: + self._relations[key_token].transaction.completed = True + del self._relations[key_token] + except KeyError: + logger.warning("No Subscriber") + diff --git a/coapthon/layers/observelayer.py.bak b/coapthon/layers/observelayer.py.bak new file mode 100644 index 0000000..c02e6e7 --- /dev/null +++ b/coapthon/layers/observelayer.py.bak @@ -0,0 +1,199 @@ +import logging +import time +from coapthon import defines + +__author__ = 'Giacomo Tanganelli' + +logger = logging.getLogger(__name__) + + +class ObserveItem(object): + def __init__(self, timestamp, non_counter, allowed, transaction): + """ + Data structure for the Observe option + + :param timestamp: the timestamop of last message sent + :param non_counter: the number of NON notification sent + :param allowed: if the client is allowed as observer + :param transaction: the transaction + """ + self.timestamp = timestamp + self.non_counter = non_counter + self.allowed = allowed + self.transaction = transaction + + +class ObserveLayer(object): + """ + Manage the observing feature. It store observing relationships. + """ + def __init__(self): + self._relations = {} + + def send_request(self, request): + """ + Add itself to the observing list + + :param request: the request + :return: the request unmodified + """ + if request.observe == 0: + # Observe request + host, port = request.destination + key_token = hash(str(host) + str(port) + str(request.token)) + + self._relations[key_token] = ObserveItem(time.time(), None, True, None) + + return request + + def receive_response(self, transaction): + """ + Sets notification's parameters. + + :type transaction: Transaction + :param transaction: the transaction + :rtype : Transaction + :return: the modified transaction + """ + host, port = transaction.response.source + key_token = hash(str(host) + str(port) + str(transaction.response.token)) + if key_token in self._relations and transaction.response.type == defines.Types["CON"]: + transaction.notification = True + return transaction + + def send_empty(self, message): + """ + Eventually remove from the observer list in case of a RST message. + + :type message: Message + :param message: the message + :return: the message unmodified + """ + host, port = message.destination + key_token = hash(str(host) + str(port) + str(message.token)) + if key_token in self._relations and message.type == defines.Types["RST"]: + del self._relations[key_token] + return message + + def receive_request(self, transaction): + """ + Manage the observe option in the request end eventually initialize the client for adding to + the list of observers or remove from the list. + + :type transaction: Transaction + :param transaction: the transaction that owns the request + :rtype : Transaction + :return: the modified transaction + """ + if transaction.request.observe == 0: + # Observe request + host, port = transaction.request.source + key_token = hash(str(host) + str(port) + str(transaction.request.token)) + non_counter = 0 + if key_token in self._relations: + # Renew registration + allowed = True + else: + allowed = False + self._relations[key_token] = ObserveItem(time.time(), non_counter, allowed, transaction) + elif transaction.request.observe == 1: + host, port = transaction.request.source + key_token = hash(str(host) + str(port) + str(transaction.request.token)) + logger.info("Remove Subscriber") + try: + del self._relations[key_token] + except KeyError: + pass + + return transaction + + def receive_empty(self, empty, transaction): + """ + Manage the observe feature to remove a client in case of a RST message receveide in reply to a notification. + + :type empty: Message + :param empty: the received message + :type transaction: Transaction + :param transaction: the transaction that owns the notification message + :rtype : Transaction + :return: the modified transaction + """ + if empty.type == defines.Types["RST"]: + host, port = transaction.request.source + key_token = hash(str(host) + str(port) + str(transaction.request.token)) + logger.info("Remove Subscriber") + try: + del self._relations[key_token] + except KeyError: + pass + transaction.completed = True + return transaction + + def send_response(self, transaction): + """ + Finalize to add the client to the list of observer. + + :type transaction: Transaction + :param transaction: the transaction that owns the response + :return: the transaction unmodified + """ + host, port = transaction.request.source + key_token = hash(str(host) + str(port) + str(transaction.request.token)) + if key_token in self._relations: + if transaction.response.code == defines.Codes.CONTENT.number: + if transaction.resource is not None and transaction.resource.observable: + + transaction.response.observe = transaction.resource.observe_count + self._relations[key_token].allowed = True + self._relations[key_token].transaction = transaction + self._relations[key_token].timestamp = time.time() + else: + del self._relations[key_token] + elif transaction.response.code >= defines.Codes.ERROR_LOWER_BOUND: + del self._relations[key_token] + return transaction + + def notify(self, resource, root=None): + """ + Prepare notification for the resource to all interested observers. + + :rtype: list + :param resource: the resource for which send a new notification + :param root: deprecated + :return: the list of transactions to be notified + """ + ret = [] + if root is not None: + resource_list = root.with_prefix_resource(resource.path) + else: + resource_list = [resource] + for key in self._relations.keys(): + if self._relations[key].transaction.resource in resource_list: + if self._relations[key].non_counter > defines.MAX_NON_NOTIFICATIONS \ + or self._relations[key].transaction.request.type == defines.Types["CON"]: + self._relations[key].transaction.response.type = defines.Types["CON"] + self._relations[key].non_counter = 0 + elif self._relations[key].transaction.request.type == defines.Types["NON"]: + self._relations[key].non_counter += 1 + self._relations[key].transaction.response.type = defines.Types["NON"] + self._relations[key].transaction.resource = resource + del self._relations[key].transaction.response.mid + del self._relations[key].transaction.response.token + ret.append(self._relations[key].transaction) + return ret + + def remove_subscriber(self, message): + """ + Remove a subscriber based on token. + + :param message: the message + """ + logger.debug("Remove Subcriber") + host, port = message.destination + key_token = hash(str(host) + str(port) + str(message.token)) + try: + self._relations[key_token].transaction.completed = True + del self._relations[key_token] + except KeyError: + logger.warning("No Subscriber") + diff --git a/coapthon/layers/requestlayer.py b/coapthon/layers/requestlayer.py new file mode 100644 index 0000000..0286b4c --- /dev/null +++ b/coapthon/layers/requestlayer.py @@ -0,0 +1,142 @@ +from coapthon.messages.response import Response +from coapthon import defines + +__author__ = 'Giacomo Tanganelli' + + +class RequestLayer(object): + """ + Class to handle the Request/Response layer + """ + def __init__(self, server): + self._server = server + + def receive_request(self, transaction): + """ + Handle request and execute the requested method + + :type transaction: Transaction + :param transaction: the transaction that owns the request + :rtype : Transaction + :return: the edited transaction with the response to the request + """ + method = transaction.request.code + if method == defines.Codes.GET.number: + transaction = self._handle_get(transaction) + elif method == defines.Codes.POST.number: + transaction = self._handle_post(transaction) + elif method == defines.Codes.PUT.number: + transaction = self._handle_put(transaction) + elif method == defines.Codes.DELETE.number: + transaction = self._handle_delete(transaction) + else: + transaction.response = None + return transaction + + def send_request(self, request): + """ + Dummy function. Used to do not broke the layered architecture. + + :type request: Request + :param request: the request + :return: the request unmodified + """ + return request + + def _handle_get(self, transaction): + """ + Handle GET requests + + :type transaction: Transaction + :param transaction: the transaction that owns the request + :rtype : Transaction + :return: the edited transaction with the response to the request + """ + path = str("/" + transaction.request.uri_path) + transaction.response = Response() + transaction.response.destination = transaction.request.source + transaction.response.token = transaction.request.token + if path == defines.DISCOVERY_URL: + transaction = self._server.resourceLayer.discover(transaction) + else: + try: + resource = self._server.root[path] + except KeyError: + resource = None + if resource is None or path == '/': + # Not Found + transaction.response.code = defines.Codes.NOT_FOUND.number + else: + transaction.resource = resource + transaction = self._server.resourceLayer.get_resource(transaction) + return transaction + + def _handle_put(self, transaction): + """ + Handle PUT requests + + :type transaction: Transaction + :param transaction: the transaction that owns the request + :rtype : Transaction + :return: the edited transaction with the response to the request + """ + path = str("/" + transaction.request.uri_path) + transaction.response = Response() + transaction.response.destination = transaction.request.source + transaction.response.token = transaction.request.token + try: + resource = self._server.root[path] + except KeyError: + resource = None + if resource is None: + transaction.response.code = defines.Codes.NOT_FOUND.number + else: + transaction.resource = resource + # Update request + transaction = self._server.resourceLayer.update_resource(transaction) + return transaction + + def _handle_post(self, transaction): + """ + Handle POST requests + + :type transaction: Transaction + :param transaction: the transaction that owns the request + :rtype : Transaction + :return: the edited transaction with the response to the request + """ + path = str("/" + transaction.request.uri_path) + transaction.response = Response() + transaction.response.destination = transaction.request.source + transaction.response.token = transaction.request.token + + # Create request + transaction = self._server.resourceLayer.create_resource(path, transaction) + return transaction + + def _handle_delete(self, transaction): + """ + Handle DELETE requests + + :type transaction: Transaction + :param transaction: the transaction that owns the request + :rtype : Transaction + :return: the edited transaction with the response to the request + """ + path = str("/" + transaction.request.uri_path) + transaction.response = Response() + transaction.response.destination = transaction.request.source + transaction.response.token = transaction.request.token + try: + resource = self._server.root[path] + except KeyError: + resource = None + + if resource is None: + transaction.response.code = defines.Codes.NOT_FOUND.number + else: + # Delete + transaction.resource = resource + transaction = self._server.resourceLayer.delete_resource(transaction, path) + return transaction + diff --git a/coapthon/layers/resourcelayer.py b/coapthon/layers/resourcelayer.py new file mode 100644 index 0000000..5e1f659 --- /dev/null +++ b/coapthon/layers/resourcelayer.py @@ -0,0 +1,564 @@ +from coapthon import defines +from coapthon.messages.response import Response +from coapthon.resources.resource import Resource + +__author__ = 'Giacomo Tanganelli' + + +class ResourceLayer(object): + """ + Handles the Resources. + """ + def __init__(self, parent): + """ + Initialize a Resource Layer. + + :type parent: CoAP + :param parent: the CoAP server + """ + self._parent = parent + + def edit_resource(self, transaction, path): + """ + Render a POST on an already created resource. + + :param path: the path of the resource + :param transaction: the transaction + :return: the transaction + """ + resource_node = self._parent.root[path] + transaction.resource = resource_node + # If-Match + if transaction.request.if_match: + if None not in transaction.request.if_match and str(transaction.resource.etag) \ + not in transaction.request.if_match: + transaction.response.code = defines.Codes.PRECONDITION_FAILED.number + return transaction + + method = getattr(resource_node, "render_POST", None) + try: + resource = method(request=transaction.request) + except NotImplementedError: + try: + method = getattr(resource_node, "render_POST_advanced", None) + ret = method(request=transaction.request, response=transaction.response) + if isinstance(ret, tuple) and len(ret) == 2 and isinstance(ret[1], Response) \ + and isinstance(ret[0], Resource): + # Advanced handler + resource, response = ret + resource.changed = True + resource.observe_count += 1 + transaction.resource = resource + transaction.response = response + if transaction.response.code is None: + transaction.response.code = defines.Codes.CREATED.number + return transaction + elif isinstance(ret, tuple) and len(ret) == 3 and isinstance(ret[1], Response) \ + and isinstance(ret[0], Resource): + # Advanced handler separate + resource, response, callback = ret + ret = self._handle_separate_advanced(transaction, callback) + if not isinstance(ret, tuple) or \ + not (isinstance(ret[0], Resource) and isinstance(ret[1], Response)): # pragma: no cover + transaction.response.code = defines.Codes.INTERNAL_SERVER_ERROR.number + return transaction + resource, response = ret + resource.changed = True + resource.observe_count += 1 + transaction.resource = resource + transaction.response = response + if transaction.response.code is None: + transaction.response.code = defines.Codes.CREATED.number + return transaction + else: + raise NotImplementedError + except NotImplementedError: + transaction.response.code = defines.Codes.METHOD_NOT_ALLOWED.number + return transaction + + if isinstance(resource, Resource): + pass + elif isinstance(resource, tuple) and len(resource) == 2: + resource, callback = resource + resource = self._handle_separate(transaction, callback) + if not isinstance(resource, Resource): # pragma: no cover + transaction.response.code = defines.Codes.INTERNAL_SERVER_ERROR.number + return transaction + else: # pragma: no cover + # Handle error + transaction.response.code = defines.Codes.INTERNAL_SERVER_ERROR.number + return transaction + + if resource.path is None: + resource.path = path + resource.observe_count = resource_node.observe_count + + if resource is resource_node: + transaction.response.code = defines.Codes.CHANGED.number + else: + transaction.response.code = defines.Codes.CREATED.number + resource.changed = True + resource.observe_count += 1 + transaction.resource = resource + + assert(isinstance(resource, Resource)) + if resource.etag is not None: + transaction.response.etag = resource.etag + + transaction.response.location_path = resource.path + + if resource.location_query is not None and len(resource.location_query) > 0: + transaction.response.location_query = resource.location_query + + transaction.response.payload = None + + self._parent.root[resource.path] = resource + + return transaction + + def add_resource(self, transaction, parent_resource, lp): + """ + Render a POST on a new resource. + + :param transaction: the transaction + :param parent_resource: the parent of the resource + :param lp: the location_path attribute of the resource + :return: the response + """ + method = getattr(parent_resource, "render_POST", None) + try: + resource = method(request=transaction.request) + except NotImplementedError: + try: + method = getattr(parent_resource, "render_POST_advanced", None) + ret = method(request=transaction.request, response=transaction.response) + if isinstance(ret, tuple) and len(ret) == 2 and isinstance(ret[1], Response) \ + and isinstance(ret[0], Resource): + # Advanced handler + resource, response = ret + resource.path = lp + resource.changed = True + self._parent.root[resource.path] = resource + transaction.resource = resource + transaction.response = response + if transaction.response.code is None: + transaction.response.code = defines.Codes.CREATED.number + return transaction + elif isinstance(ret, tuple) and len(ret) == 3 and isinstance(ret[1], Response) \ + and isinstance(ret[0], Resource): + # Advanced handler separate + resource, response, callback = ret + ret = self._handle_separate_advanced(transaction, callback) + if not isinstance(ret, tuple) or \ + not (isinstance(ret[0], Resource) and isinstance(ret[1], Response)): # pragma: no cover + transaction.response.code = defines.Codes.INTERNAL_SERVER_ERROR.number + return transaction + resource, response = ret + resource.path = lp + resource.changed = True + self._parent.root[resource.path] = resource + transaction.resource = resource + transaction.response = response + if transaction.response.code is None: + transaction.response.code = defines.Codes.CREATED.number + return transaction + else: + raise NotImplementedError + except NotImplementedError: + transaction.response.code = defines.Codes.METHOD_NOT_ALLOWED.number + return transaction + if isinstance(resource, Resource): + pass + elif isinstance(resource, tuple) and len(resource) == 2: + resource, callback = resource + resource = self._handle_separate(transaction, callback) + if not isinstance(resource, Resource): # pragma: no cover + transaction.response.code = defines.Codes.INTERNAL_SERVER_ERROR.number + return transaction + else: # pragma: no cover + # Handle error + transaction.response.code = defines.Codes.INTERNAL_SERVER_ERROR.number + return transaction + + resource.path = lp + + if resource.etag is not None: + transaction.response.etag = resource.etag + + transaction.response.location_path = resource.path + + if resource.location_query is not None and len(resource.location_query) > 0: + transaction.response.location_query = resource.location_query + + transaction.response.code = defines.Codes.CREATED.number + transaction.response.payload = None + + assert (isinstance(resource, Resource)) + if resource.etag is not None: + transaction.response.etag = resource.etag + if resource.max_age is not None: + transaction.response.max_age = resource.max_age + + resource.changed = True + + transaction.resource = resource + + self._parent.root[resource.path] = resource + + return transaction + + def create_resource(self, path, transaction): + """ + Render a POST request. + + :param path: the path of the request + :param transaction: the transaction + :return: the response + """ + t = self._parent.root.with_prefix(path) + max_len = 0 + imax = None + for i in t: + if i == path: + # Resource already present + return self.edit_resource(transaction, path) + elif len(i) > max_len: + imax = i + max_len = len(i) + + lp = path + parent_resource = self._parent.root[imax] + if parent_resource.allow_children: + return self.add_resource(transaction, parent_resource, lp) + else: + transaction.response.code = defines.Codes.METHOD_NOT_ALLOWED.number + return transaction + + def update_resource(self, transaction): + """ + Render a PUT request. + + :param transaction: the transaction + :return: the response + """ + # If-Match + if transaction.request.if_match: + if None not in transaction.request.if_match and str(transaction.resource.etag) \ + not in transaction.request.if_match: + transaction.response.code = defines.Codes.PRECONDITION_FAILED.number + return transaction + # If-None-Match + if transaction.request.if_none_match: + transaction.response.code = defines.Codes.PRECONDITION_FAILED.number + return transaction + + method = getattr(transaction.resource, "render_PUT", None) + + try: + resource = method(request=transaction.request) + except NotImplementedError: + try: + method = getattr(transaction.resource, "render_PUT_advanced", None) + ret = method(request=transaction.request, response=transaction.response) + if isinstance(ret, tuple) and len(ret) == 2 and isinstance(ret[1], Response) \ + and isinstance(ret[0], Resource): + # Advanced handler + resource, response = ret + resource.changed = True + resource.observe_count += 1 + transaction.resource = resource + transaction.response = response + if transaction.response.code is None: + transaction.response.code = defines.Codes.CHANGED.number + return transaction + elif isinstance(ret, tuple) and len(ret) == 3 and isinstance(ret[1], Response) \ + and isinstance(ret[0], Resource): + # Advanced handler separate + resource, response, callback = ret + ret = self._handle_separate_advanced(transaction, callback) + if not isinstance(ret, tuple) or \ + not (isinstance(ret[0], Resource) and isinstance(ret[1], Response)): # pragma: no cover + transaction.response.code = defines.Codes.INTERNAL_SERVER_ERROR.number + return transaction + resource, response = ret + resource.changed = True + resource.observe_count += 1 + transaction.resource = resource + transaction.response = response + if transaction.response.code is None: + transaction.response.code = defines.Codes.CHANGED.number + return transaction + else: + raise NotImplementedError + except NotImplementedError: + transaction.response.code = defines.Codes.METHOD_NOT_ALLOWED.number + return transaction + + if isinstance(resource, Resource): + pass + elif isinstance(resource, tuple) and len(resource) == 2: + resource, callback = resource + resource = self._handle_separate(transaction, callback) + if not isinstance(resource, Resource): # pragma: no cover + transaction.response.code = defines.Codes.INTERNAL_SERVER_ERROR.number + return transaction + else: # pragma: no cover + # Handle error + transaction.response.code = defines.Codes.INTERNAL_SERVER_ERROR.number + return transaction + + if resource.etag is not None: + transaction.response.etag = resource.etag + + transaction.response.code = defines.Codes.CHANGED.number + + transaction.response.payload = None + + assert (isinstance(resource, Resource)) + if resource.etag is not None: + transaction.response.etag = resource.etag + if resource.max_age is not None: + transaction.response.max_age = resource.max_age + + resource.changed = True + resource.observe_count += 1 + transaction.resource = resource + + return transaction + + def _handle_separate(self, transaction, callback): + # Handle separate + if not transaction.request.acknowledged: + self._parent._send_ack(transaction) + transaction.request.acknowledged = True + resource = callback(request=transaction.request) + return resource + + def _handle_separate_advanced(self, transaction, callback): + # Handle separate + if not transaction.request.acknowledged: + self._parent._send_ack(transaction) + transaction.request.acknowledged = True + return callback(request=transaction.request, response=transaction.response) + + def delete_resource(self, transaction, path): + """ + Render a DELETE request. + + :param transaction: the transaction + :param path: the path + :return: the response + """ + + resource = transaction.resource + method = getattr(resource, 'render_DELETE', None) + + try: + ret = method(request=transaction.request) + except NotImplementedError: + try: + method = getattr(transaction.resource, "render_DELETE_advanced", None) + ret = method(request=transaction.request, response=transaction.response) + if isinstance(ret, tuple) and len(ret) == 2 and isinstance(ret[1], Response) \ + and isinstance(ret[0], bool): + # Advanced handler + delete, response = ret + if delete: + del self._parent.root[path] + transaction.response = response + if transaction.response.code is None: + transaction.response.code = defines.Codes.DELETED.number + return transaction + elif isinstance(ret, tuple) and len(ret) == 3 and isinstance(ret[1], Response) \ + and isinstance(ret[0], Resource): + # Advanced handler separate + resource, response, callback = ret + ret = self._handle_separate_advanced(transaction, callback) + if not isinstance(ret, tuple) or \ + not (isinstance(ret[0], bool) and isinstance(ret[1], Response)): # pragma: no cover + transaction.response.code = defines.Codes.INTERNAL_SERVER_ERROR.number + return transaction + delete, response = ret + if delete: + del self._parent.root[path] + transaction.response = response + if transaction.response.code is None: + transaction.response.code = defines.Codes.DELETED.number + return transaction + else: + raise NotImplementedError + except NotImplementedError: + transaction.response.code = defines.Codes.METHOD_NOT_ALLOWED.number + return transaction + + if isinstance(ret, bool): + pass + elif isinstance(ret, tuple) and len(ret) == 2: + resource, callback = ret + ret = self._handle_separate(transaction, callback) + if not isinstance(ret, bool): # pragma: no cover + transaction.response.code = defines.Codes.INTERNAL_SERVER_ERROR.number + return transaction + else: # pragma: no cover + # Handle error + transaction.response.code = defines.Codes.INTERNAL_SERVER_ERROR.number + return transaction + if ret: + del self._parent.root[path] + transaction.response.code = defines.Codes.DELETED.number + transaction.response.payload = None + transaction.resource.deleted = True + else: # pragma: no cover + transaction.response.code = defines.Codes.INTERNAL_SERVER_ERROR.number + + return transaction + + def get_resource(self, transaction): + """ + Render a GET request. + + :param transaction: the transaction + :return: the transaction + """ + method = getattr(transaction.resource, 'render_GET', None) + + transaction.resource.actual_content_type = None + # Accept + if transaction.request.accept is not None: + transaction.resource.actual_content_type = transaction.request.accept + + # Render_GET + try: + resource = method(request=transaction.request) + except NotImplementedError: + try: + method = getattr(transaction.resource, "render_GET_advanced", None) + ret = method(request=transaction.request, response=transaction.response) + if isinstance(ret, tuple) and len(ret) == 2 and isinstance(ret[1], Response) \ + and isinstance(ret[0], Resource): + # Advanced handler + resource, response = ret + transaction.resource = resource + transaction.response = response + if transaction.response.code is None: + transaction.response.code = defines.Codes.CONTENT.number + return transaction + elif isinstance(ret, tuple) and len(ret) == 3 and isinstance(ret[1], Response) \ + and isinstance(ret[0], Resource): + # Advanced handler separate + resource, response, callback = ret + ret = self._handle_separate_advanced(transaction, callback) + if not isinstance(ret, tuple) or \ + not (isinstance(ret[0], Resource) and isinstance(ret[1], Response)): # pragma: no cover + transaction.response.code = defines.Codes.INTERNAL_SERVER_ERROR.number + return transaction + resource, response = ret + transaction.resource = resource + transaction.response = response + if transaction.response.code is None: + transaction.response.code = defines.Codes.CONTENT.number + return transaction + else: + raise NotImplementedError + except NotImplementedError: + transaction.response.code = defines.Codes.METHOD_NOT_ALLOWED.number + return transaction + + if isinstance(resource, Resource): + pass + elif isinstance(resource, tuple) and len(resource) == 2: + resource, callback = resource + resource = self._handle_separate(transaction, callback) + if not isinstance(resource, Resource): # pragma: no cover + transaction.response.code = defines.Codes.INTERNAL_SERVER_ERROR.number + return transaction + else: # pragma: no cover + # Handle error + transaction.response.code = defines.Codes.INTERNAL_SERVER_ERROR.number + return transaction.response + + if resource.etag in transaction.request.etag: + transaction.response.code = defines.Codes.VALID.number + else: + transaction.response.code = defines.Codes.CONTENT.number + + try: + transaction.response.payload = resource.payload + if resource.actual_content_type is not None \ + and resource.actual_content_type != defines.Content_types["text/plain"]: + transaction.response.content_type = resource.actual_content_type + except KeyError: + transaction.response.code = defines.Codes.NOT_ACCEPTABLE.number + return transaction.response + + assert(isinstance(resource, Resource)) + if resource.etag is not None: + transaction.response.etag = resource.etag + if resource.max_age is not None: + transaction.response.max_age = resource.max_age + + transaction.resource = resource + + return transaction + + def discover(self, transaction): + """ + Render a GET request to the .well-know/core link. + + :param transaction: the transaction + :return: the transaction + """ + transaction.response.code = defines.Codes.CONTENT.number + payload = "" + for i in self._parent.root.dump(): + if i == "/": + continue + resource = self._parent.root[i] + if resource.visible: + ret = self.valid(transaction.request.uri_query, resource.attributes) + if ret: + payload += self.corelinkformat(resource) + + transaction.response.payload = payload + transaction.response.content_type = defines.Content_types["application/link-format"] + return transaction + + @staticmethod + def valid(query, attributes): + query = query.split("&") + for q in query: + q = str(q) + assert(isinstance(q, str)) + tmp = q.split("=") + if len(tmp) > 1: + k = tmp[0] + v = tmp[1] + if k in attributes: + if v == attributes[k]: + continue + else: + return False + else: + return False + return True + + @staticmethod + def corelinkformat(resource): + """ + Return a formatted string representation of the corelinkformat in the tree. + + :return: the string + """ + msg = "<" + resource.path + ">;" + assert(isinstance(resource, Resource)) + keys = sorted(list(resource.attributes.keys())) + for k in keys: + method = getattr(resource, defines.corelinkformat[k], None) + if method is not None and method != "": + v = method + msg = msg[:-1] + ";" + str(v) + "," + else: + v = resource.attributes[k] + if v is not None: + msg = msg[:-1] + ";" + k + "=" + v + "," + return msg diff --git a/coapthon/messages/__init__.py b/coapthon/messages/__init__.py new file mode 100644 index 0000000..f89a37d --- /dev/null +++ b/coapthon/messages/__init__.py @@ -0,0 +1 @@ +__author__ = 'Giacomo Tanganelli' \ No newline at end of file diff --git a/coapthon/messages/message.py b/coapthon/messages/message.py new file mode 100644 index 0000000..c7aa32f --- /dev/null +++ b/coapthon/messages/message.py @@ -0,0 +1,695 @@ +from coapthon.utils import parse_blockwise +from coapthon import defines +from coapthon.messages.option import Option + +__author__ = 'Giacomo Tanganelli' + + +class Message(object): + """ + Class to handle the Messages. + """ + def __init__(self): + """ + Data structure that represent a CoAP message + """ + self._type = None + self._mid = None + self._token = None + self._options = [] + self._payload = None + self._destination = None + self._source = None + self._code = None + self._acknowledged = None + self._rejected = None + self._timeouted = None + self._cancelled = None + self._duplicated = None + self._timestamp = None + self._version = 1 + + @property + def version(self): + """ + Return the CoAP version + + :return: the version + """ + return self._version + + @version.setter + def version(self, v): + """ + Sets the CoAP version + + :param v: the version + :raise AttributeError: if value is not 1 + """ + if not isinstance(v, int) or v != 1: + raise AttributeError + self._version = v + + @property + def type(self): + """ + Return the type of the message. + + :return: the type + """ + return self._type + + @type.setter + def type(self, value): + """ + Sets the type of the message. + + :type value: Types + :param value: the type + :raise AttributeError: if value is not a valid type + """ + if value not in list(defines.Types.values()): + raise AttributeError + self._type = value + + @property + def mid(self): + """ + Return the mid of the message. + + :return: the MID + """ + return self._mid + + @mid.setter + def mid(self, value): + """ + Sets the MID of the message. + + :type value: Integer + :param value: the MID + :raise AttributeError: if value is not int or cannot be represented on 16 bits. + """ + if not isinstance(value, int) or value > 65536: + raise AttributeError + self._mid = value + + @mid.deleter + def mid(self): + """ + Unset the MID of the message. + """ + self._mid = None + + @property + def token(self): + """ + Get the Token of the message. + + :return: the Token + """ + return self._token + + @token.setter + def token(self, value): + """ + Set the Token of the message. + + :type value: String + :param value: the Token + :raise AttributeError: if value is longer than 256 + """ + if value is None: + self._token = value + return + if not isinstance(value, str): + value = str(value) + if len(value) > 256: + raise AttributeError + self._token = value + + @token.deleter + def token(self): + """ + Unset the Token of the message. + """ + self._token = None + + @property + def options(self): + """ + Return the options of the CoAP message. + + :rtype: list + :return: the options + """ + return self._options + + @options.setter + def options(self, value): + """ + Set the options of the CoAP message. + + :type value: list + :param value: list of options + """ + if value is None: + value = [] + assert isinstance(value, list) + self._options = value + + @property + def payload(self): + """ + Return the payload. + + :return: the payload + """ + return self._payload + + @payload.setter + def payload(self, value): + """ + Sets the payload of the message and eventually the Content-Type + + :param value: the payload + """ + if isinstance(value, tuple): + content_type, payload = value + self.content_type = content_type + self._payload = payload + else: + self._payload = value + + @property + def destination(self): + """ + Return the destination of the message. + + :rtype: tuple + :return: (ip, port) + """ + return self._destination + + @destination.setter + def destination(self, value): + """ + Set the destination of the message. + + :type value: tuple + :param value: (ip, port) + :raise AttributeError: if value is not a ip and a port. + """ + if value is not None and (not isinstance(value, tuple) or len(value)) != 2: + raise AttributeError + self._destination = value + + @property + def source(self): + """ + Return the source of the message. + + :rtype: tuple + :return: (ip, port) + """ + return self._source + + @source.setter + def source(self, value): + """ + Set the source of the message. + + :type value: tuple + :param value: (ip, port) + :raise AttributeError: if value is not a ip and a port. + """ + if not isinstance(value, tuple) or len(value) != 2: + raise AttributeError + self._source = value + + @property + def code(self): + """ + Return the code of the message. + + :rtype: Codes + :return: the code + """ + return self._code + + @code.setter + def code(self, value): + """ + Set the code of the message. + + :type value: Codes + :param value: the code + :raise AttributeError: if value is not a valid code + """ + if value not in list(defines.Codes.LIST.keys()) and value is not None: + raise AttributeError + self._code = value + + @property + def acknowledged(self): + """ + Checks if is this message has been acknowledged. + + :return: True, if is acknowledged + """ + return self._acknowledged + + @acknowledged.setter + def acknowledged(self, value): + """ + Marks this message as acknowledged. + + :type value: Boolean + :param value: if acknowledged + """ + assert (isinstance(value, bool)) + self._acknowledged = value + if value: + self._timeouted = False + self._rejected = False + self._cancelled = False + + @property + def rejected(self): + """ + Checks if this message has been rejected. + + :return: True, if is rejected + """ + return self._rejected + + @rejected.setter + def rejected(self, value): + """ + Marks this message as rejected. + + :type value: Boolean + :param value: if rejected + """ + assert (isinstance(value, bool)) + self._rejected = value + if value: + self._timeouted = False + self._acknowledged = False + self._cancelled = True + + @property + def timeouted(self): + """ + Checks if this message has timeouted. Confirmable messages in particular + might timeout. + + :return: True, if has timeouted + """ + return self._timeouted + + @timeouted.setter + def timeouted(self, value): + """ + Marks this message as timeouted. Confirmable messages in particular might + timeout. + + :type value: Boolean + :param value: + """ + assert (isinstance(value, bool)) + self._timeouted = value + if value: + self._acknowledged = False + self._rejected = False + self._cancelled = True + + @property + def duplicated(self): + """ + Checks if this message is a duplicate. + + :return: True, if is a duplicate + """ + return self._duplicated + + @duplicated.setter + def duplicated(self, value): + """ + Marks this message as a duplicate. + + :type value: Boolean + :param value: if a duplicate + """ + assert (isinstance(value, bool)) + self._duplicated = value + + @property + def timestamp(self): + """ + Return the timestamp of the message. + """ + return self._timestamp + + @timestamp.setter + def timestamp(self, value): + """ + Set the timestamp of the message. + + :type value: timestamp + :param value: the timestamp + """ + self._timestamp = value + + def _already_in(self, option): + """ + Check if an option is already in the message. + + :type option: Option + :param option: the option to be checked + :return: True if already present, False otherwise + """ + for opt in self._options: + if option.number == opt.number: + return True + return False + + def add_option(self, option): + """ + Add an option to the message. + + :type option: Option + :param option: the option + :raise TypeError: if the option is not repeatable and such option is already present in the message + """ + assert isinstance(option, Option) + repeatable = defines.OptionRegistry.LIST[option.number].repeatable + if not repeatable: + ret = self._already_in(option) + if ret: + raise TypeError("Option : %s is not repeatable", option.name) + else: + self._options.append(option) + else: + self._options.append(option) + + def del_option(self, option): + """ + Delete an option from the message + + :type option: Option + :param option: the option + """ + assert isinstance(option, Option) + while option in list(self._options): + self._options.remove(option) + + def del_option_by_name(self, name): + """ + Delete an option from the message by name + + :type name: String + :param name: option name + """ + for o in list(self._options): + assert isinstance(o, Option) + if o.name == name: + self._options.remove(o) + + def del_option_by_number(self, number): + """ + Delete an option from the message by number + + :type number: Integer + :param number: option naumber + """ + for o in list(self._options): + assert isinstance(o, Option) + if o.number == number: + self._options.remove(o) + + @property + def etag(self): + """ + Get the ETag option of the message. + + :rtype: list + :return: the ETag values or [] if not specified by the request + """ + value = [] + for option in self.options: + if option.number == defines.OptionRegistry.ETAG.number: + value.append(option.value) + return value + + @etag.setter + def etag(self, etag): + """ + Add an ETag option to the message. + + :param etag: the etag + """ + if not isinstance(etag, list): + etag = [etag] + for e in etag: + option = Option() + option.number = defines.OptionRegistry.ETAG.number + if not isinstance(e, bytes): + e = bytes(e, "utf-8") + option.value = e + self.add_option(option) + + @etag.deleter + def etag(self): + """ + Delete an ETag from a message. + + """ + self.del_option_by_number(defines.OptionRegistry.ETAG.number) + + @property + def content_type(self): + """ + Get the Content-Type option of a response. + + :return: the Content-Type value or 0 if not specified by the response + """ + value = 0 + for option in self.options: + if option.number == defines.OptionRegistry.CONTENT_TYPE.number: + value = int(option.value) + return value + + @content_type.setter + def content_type(self, content_type): + """ + Set the Content-Type option of a response. + + :type content_type: int + :param content_type: the Content-Type + """ + option = Option() + option.number = defines.OptionRegistry.CONTENT_TYPE.number + option.value = int(content_type) + self.add_option(option) + + @content_type.deleter + def content_type(self): + """ + Delete the Content-Type option of a response. + """ + + self.del_option_by_number(defines.OptionRegistry.CONTENT_TYPE.number) + + @property + def observe(self): + """ + Check if the request is an observing request. + + :return: 0, if the request is an observing request + """ + for option in self.options: + if option.number == defines.OptionRegistry.OBSERVE.number: + # if option.value is None: + # return 0 + if option.value is None: + return 0 + return option.value + return None + + @observe.setter + def observe(self, ob): + """ + Add the Observe option. + + :param ob: observe count + """ + option = Option() + option.number = defines.OptionRegistry.OBSERVE.number + option.value = ob + self.del_option_by_number(defines.OptionRegistry.OBSERVE.number) + self.add_option(option) + + @observe.deleter + def observe(self): + """ + Delete the Observe option. + """ + self.del_option_by_number(defines.OptionRegistry.OBSERVE.number) + + @property + def block1(self): + """ + Get the Block1 option. + + :return: the Block1 value + """ + value = None + for option in self.options: + if option.number == defines.OptionRegistry.BLOCK1.number: + value = parse_blockwise(option.value) + return value + + @block1.setter + def block1(self, value): + """ + Set the Block1 option. + + :param value: the Block1 value + """ + option = Option() + option.number = defines.OptionRegistry.BLOCK1.number + num, m, size = value + if size > 512: + szx = 6 + elif 256 < size <= 512: + szx = 5 + elif 128 < size <= 256: + szx = 4 + elif 64 < size <= 128: + szx = 3 + elif 32 < size <= 64: + szx = 2 + elif 16 < size <= 32: + szx = 1 + else: + szx = 0 + + value = (num << 4) + value |= (m << 3) + value |= szx + + option.value = value + self.add_option(option) + + @block1.deleter + def block1(self): + """ + Delete the Block1 option. + """ + self.del_option_by_number(defines.OptionRegistry.BLOCK1.number) + + @property + def block2(self): + """ + Get the Block2 option. + + :return: the Block2 value + """ + value = None + for option in self.options: + if option.number == defines.OptionRegistry.BLOCK2.number: + value = parse_blockwise(option.value) + return value + + @block2.setter + def block2(self, value): + """ + Set the Block2 option. + + :param value: the Block2 value + """ + option = Option() + option.number = defines.OptionRegistry.BLOCK2.number + num, m, size = value + if size > 512: + szx = 6 + elif 256 < size <= 512: + szx = 5 + elif 128 < size <= 256: + szx = 4 + elif 64 < size <= 128: + szx = 3 + elif 32 < size <= 64: + szx = 2 + elif 16 < size <= 32: + szx = 1 + else: + szx = 0 + + value = (num << 4) + value |= (m << 3) + value |= szx + + option.value = value + self.add_option(option) + + @block2.deleter + def block2(self): + """ + Delete the Block2 option. + """ + self.del_option_by_number(defines.OptionRegistry.BLOCK2.number) + + @property + def line_print(self): + """ + Return the message as a one-line string. + + :return: the string representing the message + """ + inv_types = {v: k for k, v in defines.Types.items()} + + if self._code is None: + self._code = defines.Codes.EMPTY.number + + msg = "From {source}, To {destination}, {type}-{mid}, {code}-{token}, ["\ + .format(source=self._source, destination=self._destination, type=inv_types[self._type], mid=self._mid, + code=defines.Codes.LIST[self._code].name, token=self._token) + for opt in self._options: + msg += "{name}: {value}, ".format(name=opt.name, value=opt.value) + msg += "]" + if self.payload is not None: + if isinstance(self.payload, dict): + tmp = list(self.payload.values())[0][0:20] + else: + tmp = self.payload[0:20] + msg += " {payload}...{length} bytes".format(payload=tmp, length=len(self.payload)) + else: + msg += " No payload" + return msg + + def __str__(self): + return self.line_print + + def pretty_print(self): + """ + Return the message as a formatted string. + + :return: the string representing the message + """ + msg = "Source: " + str(self._source) + "\n" + msg += "Destination: " + str(self._destination) + "\n" + inv_types = {v: k for k, v in defines.Types.items()} + msg += "Type: " + str(inv_types[self._type]) + "\n" + msg += "MID: " + str(self._mid) + "\n" + if self._code is None: + self._code = 0 + + msg += "Code: " + str(defines.Codes.LIST[self._code].name) + "\n" + msg += "Token: " + str(self._token) + "\n" + for opt in self._options: + msg += str(opt) + msg += "Payload: " + "\n" + msg += str(self._payload) + "\n" + return msg diff --git a/coapthon/messages/message.py.bak b/coapthon/messages/message.py.bak new file mode 100644 index 0000000..ea2d0a2 --- /dev/null +++ b/coapthon/messages/message.py.bak @@ -0,0 +1,693 @@ +from coapthon.utils import parse_blockwise +from coapthon import defines +from coapthon.messages.option import Option + +__author__ = 'Giacomo Tanganelli' + + +class Message(object): + """ + Class to handle the Messages. + """ + def __init__(self): + """ + Data structure that represent a CoAP message + """ + self._type = None + self._mid = None + self._token = None + self._options = [] + self._payload = None + self._destination = None + self._source = None + self._code = None + self._acknowledged = None + self._rejected = None + self._timeouted = None + self._cancelled = None + self._duplicated = None + self._timestamp = None + self._version = 1 + + @property + def version(self): + """ + Return the CoAP version + + :return: the version + """ + return self._version + + @version.setter + def version(self, v): + """ + Sets the CoAP version + + :param v: the version + :raise AttributeError: if value is not 1 + """ + if not isinstance(v, int) or v != 1: + raise AttributeError + self._version = v + + @property + def type(self): + """ + Return the type of the message. + + :return: the type + """ + return self._type + + @type.setter + def type(self, value): + """ + Sets the type of the message. + + :type value: Types + :param value: the type + :raise AttributeError: if value is not a valid type + """ + if value not in defines.Types.values(): + raise AttributeError + self._type = value + + @property + def mid(self): + """ + Return the mid of the message. + + :return: the MID + """ + return self._mid + + @mid.setter + def mid(self, value): + """ + Sets the MID of the message. + + :type value: Integer + :param value: the MID + :raise AttributeError: if value is not int or cannot be represented on 16 bits. + """ + if not isinstance(value, int) or value > 65536: + raise AttributeError + self._mid = value + + @mid.deleter + def mid(self): + """ + Unset the MID of the message. + """ + self._mid = None + + @property + def token(self): + """ + Get the Token of the message. + + :return: the Token + """ + return self._token + + @token.setter + def token(self, value): + """ + Set the Token of the message. + + :type value: String + :param value: the Token + :raise AttributeError: if value is longer than 256 + """ + if value is None: + self._token = value + return + if not isinstance(value, str): + value = str(value) + if len(value) > 256: + raise AttributeError + self._token = value + + @token.deleter + def token(self): + """ + Unset the Token of the message. + """ + self._token = None + + @property + def options(self): + """ + Return the options of the CoAP message. + + :rtype: list + :return: the options + """ + return self._options + + @options.setter + def options(self, value): + """ + Set the options of the CoAP message. + + :type value: list + :param value: list of options + """ + if value is None: + value = [] + assert isinstance(value, list) + self._options = value + + @property + def payload(self): + """ + Return the payload. + + :return: the payload + """ + return self._payload + + @payload.setter + def payload(self, value): + """ + Sets the payload of the message and eventually the Content-Type + + :param value: the payload + """ + if isinstance(value, tuple): + content_type, payload = value + self.content_type = content_type + self._payload = payload + else: + self._payload = value + + @property + def destination(self): + """ + Return the destination of the message. + + :rtype: tuple + :return: (ip, port) + """ + return self._destination + + @destination.setter + def destination(self, value): + """ + Set the destination of the message. + + :type value: tuple + :param value: (ip, port) + :raise AttributeError: if value is not a ip and a port. + """ + if value is not None and (not isinstance(value, tuple) or len(value)) != 2: + raise AttributeError + self._destination = value + + @property + def source(self): + """ + Return the source of the message. + + :rtype: tuple + :return: (ip, port) + """ + return self._source + + @source.setter + def source(self, value): + """ + Set the source of the message. + + :type value: tuple + :param value: (ip, port) + :raise AttributeError: if value is not a ip and a port. + """ + if not isinstance(value, tuple) or len(value) != 2: + raise AttributeError + self._source = value + + @property + def code(self): + """ + Return the code of the message. + + :rtype: Codes + :return: the code + """ + return self._code + + @code.setter + def code(self, value): + """ + Set the code of the message. + + :type value: Codes + :param value: the code + :raise AttributeError: if value is not a valid code + """ + if value not in defines.Codes.LIST.keys() and value is not None: + raise AttributeError + self._code = value + + @property + def acknowledged(self): + """ + Checks if is this message has been acknowledged. + + :return: True, if is acknowledged + """ + return self._acknowledged + + @acknowledged.setter + def acknowledged(self, value): + """ + Marks this message as acknowledged. + + :type value: Boolean + :param value: if acknowledged + """ + assert (isinstance(value, bool)) + self._acknowledged = value + if value: + self._timeouted = False + self._rejected = False + self._cancelled = False + + @property + def rejected(self): + """ + Checks if this message has been rejected. + + :return: True, if is rejected + """ + return self._rejected + + @rejected.setter + def rejected(self, value): + """ + Marks this message as rejected. + + :type value: Boolean + :param value: if rejected + """ + assert (isinstance(value, bool)) + self._rejected = value + if value: + self._timeouted = False + self._acknowledged = False + self._cancelled = True + + @property + def timeouted(self): + """ + Checks if this message has timeouted. Confirmable messages in particular + might timeout. + + :return: True, if has timeouted + """ + return self._timeouted + + @timeouted.setter + def timeouted(self, value): + """ + Marks this message as timeouted. Confirmable messages in particular might + timeout. + + :type value: Boolean + :param value: + """ + assert (isinstance(value, bool)) + self._timeouted = value + if value: + self._acknowledged = False + self._rejected = False + self._cancelled = True + + @property + def duplicated(self): + """ + Checks if this message is a duplicate. + + :return: True, if is a duplicate + """ + return self._duplicated + + @duplicated.setter + def duplicated(self, value): + """ + Marks this message as a duplicate. + + :type value: Boolean + :param value: if a duplicate + """ + assert (isinstance(value, bool)) + self._duplicated = value + + @property + def timestamp(self): + """ + Return the timestamp of the message. + """ + return self._timestamp + + @timestamp.setter + def timestamp(self, value): + """ + Set the timestamp of the message. + + :type value: timestamp + :param value: the timestamp + """ + self._timestamp = value + + def _already_in(self, option): + """ + Check if an option is already in the message. + + :type option: Option + :param option: the option to be checked + :return: True if already present, False otherwise + """ + for opt in self._options: + if option.number == opt.number: + return True + return False + + def add_option(self, option): + """ + Add an option to the message. + + :type option: Option + :param option: the option + :raise TypeError: if the option is not repeatable and such option is already present in the message + """ + assert isinstance(option, Option) + repeatable = defines.OptionRegistry.LIST[option.number].repeatable + if not repeatable: + ret = self._already_in(option) + if ret: + raise TypeError("Option : %s is not repeatable", option.name) + else: + self._options.append(option) + else: + self._options.append(option) + + def del_option(self, option): + """ + Delete an option from the message + + :type option: Option + :param option: the option + """ + assert isinstance(option, Option) + while option in list(self._options): + self._options.remove(option) + + def del_option_by_name(self, name): + """ + Delete an option from the message by name + + :type name: String + :param name: option name + """ + for o in list(self._options): + assert isinstance(o, Option) + if o.name == name: + self._options.remove(o) + + def del_option_by_number(self, number): + """ + Delete an option from the message by number + + :type number: Integer + :param number: option naumber + """ + for o in list(self._options): + assert isinstance(o, Option) + if o.number == number: + self._options.remove(o) + + @property + def etag(self): + """ + Get the ETag option of the message. + + :rtype: list + :return: the ETag values or [] if not specified by the request + """ + value = [] + for option in self.options: + if option.number == defines.OptionRegistry.ETAG.number: + value.append(option.value) + return value + + @etag.setter + def etag(self, etag): + """ + Add an ETag option to the message. + + :param etag: the etag + """ + if not isinstance(etag, list): + etag = [etag] + for e in etag: + option = Option() + option.number = defines.OptionRegistry.ETAG.number + option.value = e + self.add_option(option) + + @etag.deleter + def etag(self): + """ + Delete an ETag from a message. + + """ + self.del_option_by_number(defines.OptionRegistry.ETAG.number) + + @property + def content_type(self): + """ + Get the Content-Type option of a response. + + :return: the Content-Type value or 0 if not specified by the response + """ + value = 0 + for option in self.options: + if option.number == defines.OptionRegistry.CONTENT_TYPE.number: + value = int(option.value) + return value + + @content_type.setter + def content_type(self, content_type): + """ + Set the Content-Type option of a response. + + :type content_type: int + :param content_type: the Content-Type + """ + option = Option() + option.number = defines.OptionRegistry.CONTENT_TYPE.number + option.value = int(content_type) + self.add_option(option) + + @content_type.deleter + def content_type(self): + """ + Delete the Content-Type option of a response. + """ + + self.del_option_by_number(defines.OptionRegistry.CONTENT_TYPE.number) + + @property + def observe(self): + """ + Check if the request is an observing request. + + :return: 0, if the request is an observing request + """ + for option in self.options: + if option.number == defines.OptionRegistry.OBSERVE.number: + # if option.value is None: + # return 0 + if option.value is None: + return 0 + return option.value + return None + + @observe.setter + def observe(self, ob): + """ + Add the Observe option. + + :param ob: observe count + """ + option = Option() + option.number = defines.OptionRegistry.OBSERVE.number + option.value = ob + self.del_option_by_number(defines.OptionRegistry.OBSERVE.number) + self.add_option(option) + + @observe.deleter + def observe(self): + """ + Delete the Observe option. + """ + self.del_option_by_number(defines.OptionRegistry.OBSERVE.number) + + @property + def block1(self): + """ + Get the Block1 option. + + :return: the Block1 value + """ + value = None + for option in self.options: + if option.number == defines.OptionRegistry.BLOCK1.number: + value = parse_blockwise(option.value) + return value + + @block1.setter + def block1(self, value): + """ + Set the Block1 option. + + :param value: the Block1 value + """ + option = Option() + option.number = defines.OptionRegistry.BLOCK1.number + num, m, size = value + if size > 512: + szx = 6 + elif 256 < size <= 512: + szx = 5 + elif 128 < size <= 256: + szx = 4 + elif 64 < size <= 128: + szx = 3 + elif 32 < size <= 64: + szx = 2 + elif 16 < size <= 32: + szx = 1 + else: + szx = 0 + + value = (num << 4) + value |= (m << 3) + value |= szx + + option.value = value + self.add_option(option) + + @block1.deleter + def block1(self): + """ + Delete the Block1 option. + """ + self.del_option_by_number(defines.OptionRegistry.BLOCK1.number) + + @property + def block2(self): + """ + Get the Block2 option. + + :return: the Block2 value + """ + value = None + for option in self.options: + if option.number == defines.OptionRegistry.BLOCK2.number: + value = parse_blockwise(option.value) + return value + + @block2.setter + def block2(self, value): + """ + Set the Block2 option. + + :param value: the Block2 value + """ + option = Option() + option.number = defines.OptionRegistry.BLOCK2.number + num, m, size = value + if size > 512: + szx = 6 + elif 256 < size <= 512: + szx = 5 + elif 128 < size <= 256: + szx = 4 + elif 64 < size <= 128: + szx = 3 + elif 32 < size <= 64: + szx = 2 + elif 16 < size <= 32: + szx = 1 + else: + szx = 0 + + value = (num << 4) + value |= (m << 3) + value |= szx + + option.value = value + self.add_option(option) + + @block2.deleter + def block2(self): + """ + Delete the Block2 option. + """ + self.del_option_by_number(defines.OptionRegistry.BLOCK2.number) + + @property + def line_print(self): + """ + Return the message as a one-line string. + + :return: the string representing the message + """ + inv_types = {v: k for k, v in defines.Types.iteritems()} + + if self._code is None: + self._code = defines.Codes.EMPTY.number + + msg = "From {source}, To {destination}, {type}-{mid}, {code}-{token}, ["\ + .format(source=self._source, destination=self._destination, type=inv_types[self._type], mid=self._mid, + code=defines.Codes.LIST[self._code].name, token=self._token) + for opt in self._options: + msg += "{name}: {value}, ".format(name=opt.name, value=opt.value) + msg += "]" + if self.payload is not None: + if isinstance(self.payload, dict): + tmp = self.payload.values()[0][0:20] + else: + tmp = self.payload[0:20] + msg += " {payload}...{length} bytes".format(payload=tmp, length=len(self.payload)) + else: + msg += " No payload" + return msg + + def __str__(self): + return self.line_print + + def pretty_print(self): + """ + Return the message as a formatted string. + + :return: the string representing the message + """ + msg = "Source: " + str(self._source) + "\n" + msg += "Destination: " + str(self._destination) + "\n" + inv_types = {v: k for k, v in defines.Types.iteritems()} + msg += "Type: " + str(inv_types[self._type]) + "\n" + msg += "MID: " + str(self._mid) + "\n" + if self._code is None: + self._code = 0 + + msg += "Code: " + str(defines.Codes.LIST[self._code].name) + "\n" + msg += "Token: " + str(self._token) + "\n" + for opt in self._options: + msg += str(opt) + msg += "Payload: " + "\n" + msg += str(self._payload) + "\n" + return msg diff --git a/coapthon/messages/option.py b/coapthon/messages/option.py new file mode 100644 index 0000000..9906576 --- /dev/null +++ b/coapthon/messages/option.py @@ -0,0 +1,137 @@ +from coapthon import defines +from coapthon.utils import byte_len + +__author__ = 'Giacomo Tanganelli' + + +class Option(object): + """ + Class to handle the CoAP Options. + """ + def __init__(self): + """ + Data structure to store options. + """ + self._number = None + self._value = None + + @property + def number(self): + """ + Return the number of the option. + + :return: the option number + """ + return self._number + + @number.setter + def number(self, value): + """ + Set the option number. + + :type value: int + :param value: the option number + """ + self._number = value + + @property + def value(self): + """ + Return the option value. + + :return: the option value in the correct format depending on the option + """ + if type(self._value) is None: + self._value = bytearray() + opt_type = defines.OptionRegistry.LIST[self._number].value_type + if opt_type == defines.INTEGER: + if byte_len(self._value) > 0: + return int(self._value) + else: + return defines.OptionRegistry.LIST[self._number].default + return self._value + + @value.setter + def value(self, value): + """ + Set the value of the option. + + :param value: the option value + """ + opt_type = defines.OptionRegistry.LIST[self._number].value_type + if opt_type == defines.INTEGER: + if type(value) is not int: + value = int(value) + if byte_len(value) == 0: + value = 0 + elif opt_type == defines.STRING: + if type(value) is not str: + value = str(value) + elif opt_type == defines.OPAQUE: + if type(value) is bytes: + pass + else: + if value is not None: + value = bytes(value, "utf-8") + + self._value = value + + @property + def length(self): + """ + Return the value length + + :rtype : int + """ + if isinstance(self._value, int): + return byte_len(self._value) + if self._value is None: + return 0 + return len(self._value) + + def is_safe(self): + """ + Check if the option is safe. + + :rtype : bool + :return: True, if option is safe + """ + if self._number == defines.OptionRegistry.URI_HOST.number \ + or self._number == defines.OptionRegistry.URI_PORT.number \ + or self._number == defines.OptionRegistry.URI_PATH.number \ + or self._number == defines.OptionRegistry.MAX_AGE.number \ + or self._number == defines.OptionRegistry.URI_QUERY.number \ + or self._number == defines.OptionRegistry.PROXY_URI.number \ + or self._number == defines.OptionRegistry.PROXY_SCHEME.number: + return False + return True + + @property + def name(self): + """ + Return option name. + + :rtype : String + :return: the option name + """ + return defines.OptionRegistry.LIST[self._number].name + + def __str__(self): + """ + Return a string representing the option + + :rtype : String + :return: a message with the option name and the value + """ + return self.name + ": " + str(self.value) + "\n" + + def __eq__(self, other): + """ + Return True if two option are equal + + :type other: Option + :param other: the option to be compared against + :rtype : Boolean + :return: True, if option are equal + """ + return self.__dict__ == other.__dict__ diff --git a/coapthon/messages/request.py b/coapthon/messages/request.py new file mode 100644 index 0000000..9eee41d --- /dev/null +++ b/coapthon/messages/request.py @@ -0,0 +1,260 @@ +from coapthon import defines +from coapthon.messages.message import Message +from coapthon.messages.option import Option + +__author__ = 'Giacomo Tanganelli' + + +class Request(Message): + """ + Class to handle the Requests. + """ + def __init__(self): + """ + Initialize a Request message. + + """ + super(Request, self).__init__() + + @property + def uri_path(self): + """ + Return the Uri-Path of a request + + :rtype : String + :return: the Uri-Path + """ + value = [] + for option in self.options: + if option.number == defines.OptionRegistry.URI_PATH.number: + value.append(str(option.value) + '/') + value = "".join(value) + value = value[:-1] + return value + + @uri_path.setter + def uri_path(self, path): + """ + Set the Uri-Path of a request. + + :param path: the Uri-Path + """ + path = path.strip("/") + tmp = path.split("?") + path = tmp[0] + paths = path.split("/") + for p in paths: + option = Option() + option.number = defines.OptionRegistry.URI_PATH.number + option.value = p + self.add_option(option) + if len(tmp) > 1: + query = tmp[1] + self.uri_query = query + + @uri_path.deleter + def uri_path(self): + """ + Delete the Uri-Path of a request. + """ + self.del_option_by_number(defines.OptionRegistry.URI_PATH.number) + + @property + def uri_query(self): + """ + Get the Uri-Query of a request. + + :return: the Uri-Query + :rtype : String + :return: the Uri-Query string + """ + value = [] + for option in self.options: + if option.number == defines.OptionRegistry.URI_QUERY.number: + value.append(str(option.value)) + return "&".join(value) + + @uri_query.setter + def uri_query(self, value): + """ + Adds a query. + + :param value: the query + """ + del self.uri_query + queries = value.split("&") + for q in queries: + option = Option() + option.number = defines.OptionRegistry.URI_QUERY.number + option.value = str(q) + self.add_option(option) + + @uri_query.deleter + def uri_query(self): + """ + Delete a query. + """ + self.del_option_by_number(defines.OptionRegistry.URI_QUERY.number) + + @property + def accept(self): + """ + Get the Accept option of a request. + + :return: the Accept value or None if not specified by the request + :rtype : String + """ + for option in self.options: + if option.number == defines.OptionRegistry.ACCEPT.number: + return option.value + return None + + @accept.setter + def accept(self, value): + """ + Add an Accept option to a request. + + :param value: the Accept value + """ + if value in list(defines.Content_types.values()): + option = Option() + option.number = defines.OptionRegistry.ACCEPT.number + option.value = value + self.add_option(option) + + @accept.deleter + def accept(self): + """ + Delete the Accept options of a request. + """ + self.del_option_by_number(defines.OptionRegistry.ACCEPT.number) + + @property + def if_match(self): + """ + Get the If-Match option of a request. + + :return: the If-Match values or [] if not specified by the request + :rtype : list + """ + value = [] + for option in self.options: + if option.number == defines.OptionRegistry.IF_MATCH.number: + value.append(option.value) + return value + + @if_match.setter + def if_match(self, values): + """ + Set the If-Match option of a request. + + :param values: the If-Match values + :type values : list + """ + assert isinstance(values, list) + for v in values: + option = Option() + option.number = defines.OptionRegistry.IF_MATCH.number + option.value = v + self.add_option(option) + + @if_match.deleter + def if_match(self): + """ + Delete the If-Match options of a request. + """ + self.del_option_by_number(defines.OptionRegistry.IF_MATCH.number) + + @property + def if_none_match(self): + """ + Get the if-none-match option of a request. + + :return: True, if if-none-match is present + :rtype : bool + """ + for option in self.options: + if option.number == defines.OptionRegistry.IF_NONE_MATCH.number: + return True + return False + + def add_if_none_match(self): + """ + Add the if-none-match option to the request. + """ + option = Option() + option.number = defines.OptionRegistry.IF_NONE_MATCH.number + option.value = None + self.add_option(option) + + @if_none_match.deleter + def if_none_match(self): + """ + Delete the if-none-match option in the request. + """ + self.del_option_by_number(defines.OptionRegistry.IF_NONE_MATCH.number) + + @property + def proxy_uri(self): + """ + Get the Proxy-Uri option of a request. + + :return: the Proxy-Uri values or None if not specified by the request + :rtype : String + """ + for option in self.options: + if option.number == defines.OptionRegistry.PROXY_URI.number: + return option.value + return None + + @proxy_uri.setter + def proxy_uri(self, value): + """ + Set the Proxy-Uri option of a request. + + :param value: the Proxy-Uri value + """ + option = Option() + option.number = defines.OptionRegistry.PROXY_URI.number + option.value = str(value) + self.add_option(option) + + @proxy_uri.deleter + def proxy_uri(self): + """ + Delete the Proxy-Uri option of a request. + """ + self.del_option_by_number(defines.OptionRegistry.PROXY_URI.number) + + @property + def proxy_schema(self): + """ + Get the Proxy-Schema option of a request. + + :return: the Proxy-Schema values or None if not specified by the request + :rtype : String + """ + for option in self.options: + if option.number == defines.OptionRegistry.PROXY_SCHEME.number: + return option.value + return None + + @proxy_schema.setter + def proxy_schema(self, value): + """ + Set the Proxy-Schema option of a request. + + :param value: the Proxy-Schema value + """ + option = Option() + option.number = defines.OptionRegistry.PROXY_SCHEME.number + option.value = str(value) + self.add_option(option) + + @proxy_schema.deleter + def proxy_schema(self): + """ + Delete the Proxy-Schema option of a request. + """ + self.del_option_by_number(defines.OptionRegistry.PROXY_SCHEME.number) + diff --git a/coapthon/messages/request.py.bak b/coapthon/messages/request.py.bak new file mode 100644 index 0000000..07ee8fa --- /dev/null +++ b/coapthon/messages/request.py.bak @@ -0,0 +1,260 @@ +from coapthon import defines +from coapthon.messages.message import Message +from coapthon.messages.option import Option + +__author__ = 'Giacomo Tanganelli' + + +class Request(Message): + """ + Class to handle the Requests. + """ + def __init__(self): + """ + Initialize a Request message. + + """ + super(Request, self).__init__() + + @property + def uri_path(self): + """ + Return the Uri-Path of a request + + :rtype : String + :return: the Uri-Path + """ + value = [] + for option in self.options: + if option.number == defines.OptionRegistry.URI_PATH.number: + value.append(str(option.value) + '/') + value = "".join(value) + value = value[:-1] + return value + + @uri_path.setter + def uri_path(self, path): + """ + Set the Uri-Path of a request. + + :param path: the Uri-Path + """ + path = path.strip("/") + tmp = path.split("?") + path = tmp[0] + paths = path.split("/") + for p in paths: + option = Option() + option.number = defines.OptionRegistry.URI_PATH.number + option.value = p + self.add_option(option) + if len(tmp) > 1: + query = tmp[1] + self.uri_query = query + + @uri_path.deleter + def uri_path(self): + """ + Delete the Uri-Path of a request. + """ + self.del_option_by_number(defines.OptionRegistry.URI_PATH.number) + + @property + def uri_query(self): + """ + Get the Uri-Query of a request. + + :return: the Uri-Query + :rtype : String + :return: the Uri-Query string + """ + value = [] + for option in self.options: + if option.number == defines.OptionRegistry.URI_QUERY.number: + value.append(str(option.value)) + return "&".join(value) + + @uri_query.setter + def uri_query(self, value): + """ + Adds a query. + + :param value: the query + """ + del self.uri_query + queries = value.split("&") + for q in queries: + option = Option() + option.number = defines.OptionRegistry.URI_QUERY.number + option.value = str(q) + self.add_option(option) + + @uri_query.deleter + def uri_query(self): + """ + Delete a query. + """ + self.del_option_by_number(defines.OptionRegistry.URI_QUERY.number) + + @property + def accept(self): + """ + Get the Accept option of a request. + + :return: the Accept value or None if not specified by the request + :rtype : String + """ + for option in self.options: + if option.number == defines.OptionRegistry.ACCEPT.number: + return option.value + return None + + @accept.setter + def accept(self, value): + """ + Add an Accept option to a request. + + :param value: the Accept value + """ + if value in defines.Content_types.values(): + option = Option() + option.number = defines.OptionRegistry.ACCEPT.number + option.value = value + self.add_option(option) + + @accept.deleter + def accept(self): + """ + Delete the Accept options of a request. + """ + self.del_option_by_number(defines.OptionRegistry.ACCEPT.number) + + @property + def if_match(self): + """ + Get the If-Match option of a request. + + :return: the If-Match values or [] if not specified by the request + :rtype : list + """ + value = [] + for option in self.options: + if option.number == defines.OptionRegistry.IF_MATCH.number: + value.append(option.value) + return value + + @if_match.setter + def if_match(self, values): + """ + Set the If-Match option of a request. + + :param values: the If-Match values + :type values : list + """ + assert isinstance(values, list) + for v in values: + option = Option() + option.number = defines.OptionRegistry.IF_MATCH.number + option.value = v + self.add_option(option) + + @if_match.deleter + def if_match(self): + """ + Delete the If-Match options of a request. + """ + self.del_option_by_number(defines.OptionRegistry.IF_MATCH.number) + + @property + def if_none_match(self): + """ + Get the if-none-match option of a request. + + :return: True, if if-none-match is present + :rtype : bool + """ + for option in self.options: + if option.number == defines.OptionRegistry.IF_NONE_MATCH.number: + return True + return False + + def add_if_none_match(self): + """ + Add the if-none-match option to the request. + """ + option = Option() + option.number = defines.OptionRegistry.IF_NONE_MATCH.number + option.value = None + self.add_option(option) + + @if_none_match.deleter + def if_none_match(self): + """ + Delete the if-none-match option in the request. + """ + self.del_option_by_number(defines.OptionRegistry.IF_NONE_MATCH.number) + + @property + def proxy_uri(self): + """ + Get the Proxy-Uri option of a request. + + :return: the Proxy-Uri values or None if not specified by the request + :rtype : String + """ + for option in self.options: + if option.number == defines.OptionRegistry.PROXY_URI.number: + return option.value + return None + + @proxy_uri.setter + def proxy_uri(self, value): + """ + Set the Proxy-Uri option of a request. + + :param value: the Proxy-Uri value + """ + option = Option() + option.number = defines.OptionRegistry.PROXY_URI.number + option.value = str(value) + self.add_option(option) + + @proxy_uri.deleter + def proxy_uri(self): + """ + Delete the Proxy-Uri option of a request. + """ + self.del_option_by_number(defines.OptionRegistry.PROXY_URI.number) + + @property + def proxy_schema(self): + """ + Get the Proxy-Schema option of a request. + + :return: the Proxy-Schema values or None if not specified by the request + :rtype : String + """ + for option in self.options: + if option.number == defines.OptionRegistry.PROXY_SCHEME.number: + return option.value + return None + + @proxy_schema.setter + def proxy_schema(self, value): + """ + Set the Proxy-Schema option of a request. + + :param value: the Proxy-Schema value + """ + option = Option() + option.number = defines.OptionRegistry.PROXY_SCHEME.number + option.value = str(value) + self.add_option(option) + + @proxy_schema.deleter + def proxy_schema(self): + """ + Delete the Proxy-Schema option of a request. + """ + self.del_option_by_number(defines.OptionRegistry.PROXY_SCHEME.number) + diff --git a/coapthon/messages/response.py b/coapthon/messages/response.py new file mode 100644 index 0000000..8e73b54 --- /dev/null +++ b/coapthon/messages/response.py @@ -0,0 +1,124 @@ +from coapthon import defines +from coapthon.messages.message import Message +from coapthon.messages.option import Option + +__author__ = 'Giacomo Tanganelli' + + +class Response(Message): + """ + Class to handle the Responses. + """ + @property + def location_path(self): + """ + Return the Location-Path of the response. + + :rtype : String + :return: the Location-Path option + """ + value = [] + for option in self.options: + if option.number == defines.OptionRegistry.LOCATION_PATH.number: + value.append(str(option.value)) + return "/".join(value) + + @location_path.setter + def location_path(self, path): + """ + Set the Location-Path of the response. + + :type path: String + :param path: the Location-Path as a string + """ + path = path.strip("/") + tmp = path.split("?") + path = tmp[0] + paths = path.split("/") + for p in paths: + option = Option() + option.number = defines.OptionRegistry.LOCATION_PATH.number + option.value = p + self.add_option(option) + # if len(tmp) > 1: + # query = tmp[1] + # self.location_query = query + + @location_path.deleter + def location_path(self): + """ + Delete the Location-Path of the response. + """ + self.del_option_by_number(defines.OptionRegistry.LOCATION_PATH.number) + + @property + def location_query(self): + """ + Return the Location-Query of the response. + + :rtype : String + :return: the Location-Query option + """ + value = [] + for option in self.options: + if option.number == defines.OptionRegistry.LOCATION_QUERY.number: + value.append(option.value) + return value + + @location_query.setter + def location_query(self, value): + """ + Set the Location-Query of the response. + + :type path: String + :param path: the Location-Query as a string + """ + del self.location_query + queries = value.split("&") + for q in queries: + option = Option() + option.number = defines.OptionRegistry.LOCATION_QUERY.number + option.value = str(q) + self.add_option(option) + + @location_query.deleter + def location_query(self): + """ + Delete the Location-Query of the response. + """ + self.del_option_by_number(defines.OptionRegistry.LOCATION_QUERY.number) + + @property + def max_age(self): + """ + Return the MaxAge of the response. + + :rtype : int + :return: the MaxAge option + """ + value = defines.OptionRegistry.MAX_AGE.default + for option in self.options: + if option.number == defines.OptionRegistry.MAX_AGE.number: + value = int(option.value) + return value + + @max_age.setter + def max_age(self, value): + """ + Set the MaxAge of the response. + + :type value: int + :param value: the MaxAge option + """ + option = Option() + option.number = defines.OptionRegistry.MAX_AGE.number + option.value = int(value) + self.del_option_by_number(defines.OptionRegistry.MAX_AGE.number) + self.add_option(option) + + @max_age.deleter + def max_age(self): + """ + Delete the MaxAge of the response. + """ + self.del_option_by_number(defines.OptionRegistry.MAX_AGE.number) diff --git a/coapthon/resources/__init__.py b/coapthon/resources/__init__.py new file mode 100644 index 0000000..0dc55fa --- /dev/null +++ b/coapthon/resources/__init__.py @@ -0,0 +1 @@ +__author__ = 'Giacomo Tanganelli' diff --git a/coapthon/resources/remoteResource.py b/coapthon/resources/remoteResource.py new file mode 100644 index 0000000..fc91c4f --- /dev/null +++ b/coapthon/resources/remoteResource.py @@ -0,0 +1,11 @@ +from coapthon.resources.resource import Resource + +__author__ = 'Giacomo Tanganelli' + + +class RemoteResource(Resource): + def __init__(self, name, remote_server, remote_path, coap_server=None, visible=True, observable=True, allow_children=True): + super(RemoteResource, self).__init__(name, coap_server, visible=visible, observable=observable, + allow_children=allow_children) + self.remote_path = remote_path + self.remote_server = remote_server diff --git a/coapthon/resources/resource.py b/coapthon/resources/resource.py new file mode 100644 index 0000000..2f820fc --- /dev/null +++ b/coapthon/resources/resource.py @@ -0,0 +1,516 @@ +from coapthon import defines + +__author__ = 'Giacomo Tanganelli' + + +class Resource(object): + """ + The Resource class. Represents the base class for all resources. + """ + def __init__(self, name, coap_server=None, visible=True, observable=True, allow_children=True): + """ + Initialize a new Resource. + + :param name: the name of the resource. + :param coap_server: the server that own the resource + :param visible: if the resource is visible + :param observable: if the resource is observable + :param allow_children: if the resource could has children + """ + # The attributes of this resource. + self._attributes = {} + + # The resource name. + self.name = name + + # The resource path. + self.path = None + + # Indicates whether this resource is visible to clients. + self._visible = visible + + # Indicates whether this resource is observable by clients. + self._observable = observable + if self._observable: + self._attributes["obs"] = "" + + self._allow_children = allow_children + + self._observe_count = 1 + + self._payload = {} + + self._content_type = None + + self._etag = [] + + self._location_query = [] + + self._max_age = None + + self._coap_server = coap_server + + self._deleted = False + + self._changed = False + + @property + def deleted(self): + """ + Check if the resource has been deleted. For observing purpose. + + :rtype: bool + :return: True, if deleted + """ + return self._deleted + + @deleted.setter + def deleted(self, b): + """ + Set the deleted parameter. For observing purpose. + + :type b: bool + :param b: True, if deleted + """ + self._deleted = b + + @property + def changed(self): + """ + Check if the resource has been changed. For observing purpose. + + :rtype: bool + :return: True, if changed + """ + return self._changed + + @changed.setter + def changed(self, b): + """ + Set the changed parameter. For observing purpose. + + :type b: bool + :param b: True, if changed + """ + self._changed = b + + @property + def etag(self): + """ + Get the last valid ETag of the resource. + + :return: the last ETag value or None if the resource doesn't have any ETag + """ + if self._etag: + return self._etag[-1] + else: + return None + + @etag.setter + def etag(self, etag): + """ + Set the ETag of the resource. + + :param etag: the ETag + """ + if not isinstance(etag, bytes): + etag = bytes(etag, "utf-8") + self._etag.append(etag) + + @property + def location_query(self): + """ + Get the Location-Query of a resource. + + :return: the Location-Query + """ + return self._location_query + + @location_query.setter + def location_query(self, lq): + """ + Set the Location-Query. + + :param lq: the Location-Query + """ + self._location_query = lq + + @location_query.deleter + def location_query(self): + """ + Delete the Location-Query. + + """ + self.location_query = [] + + @property + def max_age(self): + """ + Get the Max-Age. + + :return: the Max-Age + """ + return self._max_age + + @max_age.setter + def max_age(self, ma): + """ + Set the Max-Age. + + :param ma: the Max-Age + """ + self._max_age = ma + + @property + def payload(self): + """ + Get the payload of the resource according to the content type specified by required_content_type or + "text/plain" by default. + + :return: the payload. + """ + if self._content_type is not None: + try: + return self._payload[self._content_type] + except KeyError: + raise KeyError("Content-Type not available") + else: + + if defines.Content_types["text/plain"] in self._payload: + return self._payload[defines.Content_types["text/plain"]] + else: + val = list(self._payload.keys()) + return val[0], self._payload[val[0]] + + @payload.setter + def payload(self, p): + """ + Set the payload of the resource. + + :param p: the new payload + """ + if isinstance(p, tuple): + k = p[0] + v = p[1] + self.actual_content_type = k + self._payload[k] = v + else: + self._payload = {defines.Content_types["text/plain"]: p} + + @property + def attributes(self): + """ + Get the CoRE Link Format attribute of the resource. + + :return: the attribute of the resource + """ + return self._attributes + + @attributes.setter + def attributes(self, att): + # TODO assert + """ + Set the CoRE Link Format attribute of the resource. + + :param att: the attributes + """ + self._attributes = att + + @property + def visible(self): + """ + Get if the resource is visible. + + :return: True, if visible + """ + return self._visible + + @property + def observable(self): + """ + Get if the resource is observable. + + :return: True, if observable + """ + return self._observable + + @property + def allow_children(self): + """ + Get if the resource allow children. + + :return: True, if allow children + """ + return self._allow_children + + @property + def observe_count(self): + """ + Get the Observe counter. + + :return: the Observe counter value + """ + return self._observe_count + + @observe_count.setter + def observe_count(self, v): + """ + Set the Observe counter. + + :param v: the Observe counter value + """ + assert isinstance(v, int) + self._observe_count = (v % 65000) + + @property + def actual_content_type(self): + """ + Get the actual required Content-Type. + + :return: the actual required Content-Type. + """ + return self._content_type + + @actual_content_type.setter + def actual_content_type(self, act): + """ + Set the actual required Content-Type. + + :param act: the actual required Content-Type. + """ + self._content_type = act + + @property + def content_type(self): + """ + Get the CoRE Link Format ct attribute of the resource. + + :return: the CoRE Link Format ct attribute + """ + value = "" + lst = self._attributes.get("ct") + if lst is not None and len(lst) > 0: + value = "ct=" + for v in lst: + value += str(v) + " " + if len(value) > 0: + value = value[:-1] + return value + + @content_type.setter + def content_type(self, lst): + """ + Set the CoRE Link Format ct attribute of the resource. + + :param lst: the list of CoRE Link Format ct attribute of the resource + """ + value = [] + if isinstance(lst, str): + ct = defines.Content_types[lst] + self.add_content_type(ct) + elif isinstance(lst, list): + for ct in lst: + self.add_content_type(ct) + + def add_content_type(self, ct): + """ + Add a CoRE Link Format ct attribute to the resource. + + :param ct: the CoRE Link Format ct attribute + """ + lst = self._attributes.get("ct") + if lst is None: + lst = [] + if isinstance(ct, str): + ct = defines.Content_types[ct] + lst.append(ct) + self._attributes["ct"] = lst + + @property + def resource_type(self): + """ + Get the CoRE Link Format rt attribute of the resource. + + :return: the CoRE Link Format rt attribute + """ + value = "rt=" + lst = self._attributes.get("rt") + if lst is None: + value = "" + else: + value += "\"" + str(lst) + "\"" + return value + + @resource_type.setter + def resource_type(self, rt): + """ + Set the CoRE Link Format rt attribute of the resource. + + :param rt: the CoRE Link Format rt attribute + """ + if not isinstance(rt, str): + rt = str(rt) + self._attributes["rt"] = rt + + @property + def interface_type(self): + """ + Get the CoRE Link Format if attribute of the resource. + + :return: the CoRE Link Format if attribute + """ + value = "if=" + lst = self._attributes.get("if") + if lst is None: + value = "" + else: + value += "\"" + str(lst) + "\"" + return value + + @interface_type.setter + def interface_type(self, ift): + """ + Set the CoRE Link Format if attribute of the resource. + + :param ift: the CoRE Link Format if attribute + """ + if not isinstance(ift, str): + ift = str(ift) + self._attributes["if"] = ift + + @property + def maximum_size_estimated(self): + """ + Get the CoRE Link Format sz attribute of the resource. + + :return: the CoRE Link Format sz attribute + """ + value = "sz=" + lst = self._attributes.get("sz") + if lst is None: + value = "" + else: + value += "\"" + str(lst) + "\"" + return value + + @maximum_size_estimated.setter + def maximum_size_estimated(self, sz): + """ + Set the CoRE Link Format sz attribute of the resource. + + :param sz: the CoRE Link Format sz attribute + """ + if not isinstance(sz, str): + sz = str(sz) + self._attributes["sz"] = sz + + @property + def observing(self): + """ + Get the CoRE Link Format obs attribute of the resource. + + :return: the CoRE Link Format obs attribute + """ + if self._observable: + return "obs" + + def init_resource(self, request, res): + """ + Helper function to initialize a new resource. + + :param request: the request that generate the new resource + :param res: the resource + :return: the edited resource + """ + res.location_query = request.uri_query + res.payload = (request.content_type, request.payload) + return res + + def edit_resource(self, request): + """ + Helper function to edit a resource + + :param request: the request that edit the resource + """ + self.location_query = request.uri_query + self.payload = (request.content_type, request.payload) + + def render_GET(self, request): + """ + Method to be redefined to render a GET request on the resource. + + :param request: the request + :return: the resource + """ + raise NotImplementedError + + def render_GET_advanced(self, request, response): + """ + Method to be redefined to render a GET request on the resource. + + :param response: the partially filled response + :param request: the request + :return: a tuple with (the resource, the response) + """ + raise NotImplementedError + + def render_PUT(self, request): + """ + Method to be redefined to render a PUTT request on the resource. + + :param request: the request + :return: the resource + """ + raise NotImplementedError + + def render_PUT_advanced(self, request, response): + """ + Method to be redefined to render a PUTT request on the resource. + + :param response: the partially filled response + :param request: the request + :return: a tuple with (the resource, the response) + """ + raise NotImplementedError + + def render_POST(self, request): + """ + Method to be redefined to render a POST request on the resource. + + :param request: the request + :return: the resource + """ + raise NotImplementedError + + def render_POST_advanced(self, request, response): + """ + Method to be redefined to render a POST request on the resource. + + :param response: the partially filled response + :param request: the request + :return: a tuple with (the resource, the response) + """ + raise NotImplementedError + + def render_DELETE(self, request): + """ + Method to be redefined to render a DELETE request on the resource. + + :param request: the request + :return: a boolean + """ + raise NotImplementedError + + def render_DELETE_advanced(self, request, response): + """ + Method to be redefined to render a DELETE request on the resource. + + :param response: the partially filled response + :param request: the request + :return: a tuple with a boolean and the response + """ + raise NotImplementedError + + + + diff --git a/coapthon/resources/resource.py.bak b/coapthon/resources/resource.py.bak new file mode 100644 index 0000000..9a02785 --- /dev/null +++ b/coapthon/resources/resource.py.bak @@ -0,0 +1,514 @@ +from coapthon import defines + +__author__ = 'Giacomo Tanganelli' + + +class Resource(object): + """ + The Resource class. Represents the base class for all resources. + """ + def __init__(self, name, coap_server=None, visible=True, observable=True, allow_children=True): + """ + Initialize a new Resource. + + :param name: the name of the resource. + :param coap_server: the server that own the resource + :param visible: if the resource is visible + :param observable: if the resource is observable + :param allow_children: if the resource could has children + """ + # The attributes of this resource. + self._attributes = {} + + # The resource name. + self.name = name + + # The resource path. + self.path = None + + # Indicates whether this resource is visible to clients. + self._visible = visible + + # Indicates whether this resource is observable by clients. + self._observable = observable + if self._observable: + self._attributes["obs"] = "" + + self._allow_children = allow_children + + self._observe_count = 1 + + self._payload = {} + + self._content_type = None + + self._etag = [] + + self._location_query = [] + + self._max_age = None + + self._coap_server = coap_server + + self._deleted = False + + self._changed = False + + @property + def deleted(self): + """ + Check if the resource has been deleted. For observing purpose. + + :rtype: bool + :return: True, if deleted + """ + return self._deleted + + @deleted.setter + def deleted(self, b): + """ + Set the deleted parameter. For observing purpose. + + :type b: bool + :param b: True, if deleted + """ + self._deleted = b + + @property + def changed(self): + """ + Check if the resource has been changed. For observing purpose. + + :rtype: bool + :return: True, if changed + """ + return self._changed + + @changed.setter + def changed(self, b): + """ + Set the changed parameter. For observing purpose. + + :type b: bool + :param b: True, if changed + """ + self._changed = b + + @property + def etag(self): + """ + Get the last valid ETag of the resource. + + :return: the last ETag value or None if the resource doesn't have any ETag + """ + if self._etag: + return self._etag[-1] + else: + return None + + @etag.setter + def etag(self, etag): + """ + Set the ETag of the resource. + + :param etag: the ETag + """ + self._etag.append(etag) + + @property + def location_query(self): + """ + Get the Location-Query of a resource. + + :return: the Location-Query + """ + return self._location_query + + @location_query.setter + def location_query(self, lq): + """ + Set the Location-Query. + + :param lq: the Location-Query + """ + self._location_query = lq + + @location_query.deleter + def location_query(self): + """ + Delete the Location-Query. + + """ + self.location_query = [] + + @property + def max_age(self): + """ + Get the Max-Age. + + :return: the Max-Age + """ + return self._max_age + + @max_age.setter + def max_age(self, ma): + """ + Set the Max-Age. + + :param ma: the Max-Age + """ + self._max_age = ma + + @property + def payload(self): + """ + Get the payload of the resource according to the content type specified by required_content_type or + "text/plain" by default. + + :return: the payload. + """ + if self._content_type is not None: + try: + return self._payload[self._content_type] + except KeyError: + raise KeyError("Content-Type not available") + else: + + if defines.Content_types["text/plain"] in self._payload: + return self._payload[defines.Content_types["text/plain"]] + else: + val = self._payload.keys() + return val[0], self._payload[val[0]] + + @payload.setter + def payload(self, p): + """ + Set the payload of the resource. + + :param p: the new payload + """ + if isinstance(p, tuple): + k = p[0] + v = p[1] + self.actual_content_type = k + self._payload[k] = v + else: + self._payload = {defines.Content_types["text/plain"]: p} + + @property + def attributes(self): + """ + Get the CoRE Link Format attribute of the resource. + + :return: the attribute of the resource + """ + return self._attributes + + @attributes.setter + def attributes(self, att): + # TODO assert + """ + Set the CoRE Link Format attribute of the resource. + + :param att: the attributes + """ + self._attributes = att + + @property + def visible(self): + """ + Get if the resource is visible. + + :return: True, if visible + """ + return self._visible + + @property + def observable(self): + """ + Get if the resource is observable. + + :return: True, if observable + """ + return self._observable + + @property + def allow_children(self): + """ + Get if the resource allow children. + + :return: True, if allow children + """ + return self._allow_children + + @property + def observe_count(self): + """ + Get the Observe counter. + + :return: the Observe counter value + """ + return self._observe_count + + @observe_count.setter + def observe_count(self, v): + """ + Set the Observe counter. + + :param v: the Observe counter value + """ + assert isinstance(v, int) + self._observe_count = (v % 65000) + + @property + def actual_content_type(self): + """ + Get the actual required Content-Type. + + :return: the actual required Content-Type. + """ + return self._content_type + + @actual_content_type.setter + def actual_content_type(self, act): + """ + Set the actual required Content-Type. + + :param act: the actual required Content-Type. + """ + self._content_type = act + + @property + def content_type(self): + """ + Get the CoRE Link Format ct attribute of the resource. + + :return: the CoRE Link Format ct attribute + """ + value = "" + lst = self._attributes.get("ct") + if lst is not None and len(lst) > 0: + value = "ct=" + for v in lst: + value += str(v) + " " + if len(value) > 0: + value = value[:-1] + return value + + @content_type.setter + def content_type(self, lst): + """ + Set the CoRE Link Format ct attribute of the resource. + + :param lst: the list of CoRE Link Format ct attribute of the resource + """ + value = [] + if isinstance(lst, str): + ct = defines.Content_types[lst] + self.add_content_type(ct) + elif isinstance(lst, list): + for ct in lst: + self.add_content_type(ct) + + def add_content_type(self, ct): + """ + Add a CoRE Link Format ct attribute to the resource. + + :param ct: the CoRE Link Format ct attribute + """ + lst = self._attributes.get("ct") + if lst is None: + lst = [] + if isinstance(ct, str): + ct = defines.Content_types[ct] + lst.append(ct) + self._attributes["ct"] = lst + + @property + def resource_type(self): + """ + Get the CoRE Link Format rt attribute of the resource. + + :return: the CoRE Link Format rt attribute + """ + value = "rt=" + lst = self._attributes.get("rt") + if lst is None: + value = "" + else: + value += "\"" + str(lst) + "\"" + return value + + @resource_type.setter + def resource_type(self, rt): + """ + Set the CoRE Link Format rt attribute of the resource. + + :param rt: the CoRE Link Format rt attribute + """ + if not isinstance(rt, str): + rt = str(rt) + self._attributes["rt"] = rt + + @property + def interface_type(self): + """ + Get the CoRE Link Format if attribute of the resource. + + :return: the CoRE Link Format if attribute + """ + value = "if=" + lst = self._attributes.get("if") + if lst is None: + value = "" + else: + value += "\"" + str(lst) + "\"" + return value + + @interface_type.setter + def interface_type(self, ift): + """ + Set the CoRE Link Format if attribute of the resource. + + :param ift: the CoRE Link Format if attribute + """ + if not isinstance(ift, str): + ift = str(ift) + self._attributes["if"] = ift + + @property + def maximum_size_estimated(self): + """ + Get the CoRE Link Format sz attribute of the resource. + + :return: the CoRE Link Format sz attribute + """ + value = "sz=" + lst = self._attributes.get("sz") + if lst is None: + value = "" + else: + value += "\"" + str(lst) + "\"" + return value + + @maximum_size_estimated.setter + def maximum_size_estimated(self, sz): + """ + Set the CoRE Link Format sz attribute of the resource. + + :param sz: the CoRE Link Format sz attribute + """ + if not isinstance(sz, str): + sz = str(sz) + self._attributes["sz"] = sz + + @property + def observing(self): + """ + Get the CoRE Link Format obs attribute of the resource. + + :return: the CoRE Link Format obs attribute + """ + if self._observable: + return "obs" + + def init_resource(self, request, res): + """ + Helper function to initialize a new resource. + + :param request: the request that generate the new resource + :param res: the resource + :return: the edited resource + """ + res.location_query = request.uri_query + res.payload = (request.content_type, request.payload) + return res + + def edit_resource(self, request): + """ + Helper function to edit a resource + + :param request: the request that edit the resource + """ + self.location_query = request.uri_query + self.payload = (request.content_type, request.payload) + + def render_GET(self, request): + """ + Method to be redefined to render a GET request on the resource. + + :param request: the request + :return: the resource + """ + raise NotImplementedError + + def render_GET_advanced(self, request, response): + """ + Method to be redefined to render a GET request on the resource. + + :param response: the partially filled response + :param request: the request + :return: a tuple with (the resource, the response) + """ + raise NotImplementedError + + def render_PUT(self, request): + """ + Method to be redefined to render a PUTT request on the resource. + + :param request: the request + :return: the resource + """ + raise NotImplementedError + + def render_PUT_advanced(self, request, response): + """ + Method to be redefined to render a PUTT request on the resource. + + :param response: the partially filled response + :param request: the request + :return: a tuple with (the resource, the response) + """ + raise NotImplementedError + + def render_POST(self, request): + """ + Method to be redefined to render a POST request on the resource. + + :param request: the request + :return: the resource + """ + raise NotImplementedError + + def render_POST_advanced(self, request, response): + """ + Method to be redefined to render a POST request on the resource. + + :param response: the partially filled response + :param request: the request + :return: a tuple with (the resource, the response) + """ + raise NotImplementedError + + def render_DELETE(self, request): + """ + Method to be redefined to render a DELETE request on the resource. + + :param request: the request + :return: a boolean + """ + raise NotImplementedError + + def render_DELETE_advanced(self, request, response): + """ + Method to be redefined to render a DELETE request on the resource. + + :param response: the partially filled response + :param request: the request + :return: a tuple with a boolean and the response + """ + raise NotImplementedError + + + + diff --git a/coapthon/reverse_proxy/__init__.py b/coapthon/reverse_proxy/__init__.py new file mode 100644 index 0000000..0dc55fa --- /dev/null +++ b/coapthon/reverse_proxy/__init__.py @@ -0,0 +1 @@ +__author__ = 'Giacomo Tanganelli' diff --git a/coapthon/reverse_proxy/coap.py b/coapthon/reverse_proxy/coap.py new file mode 100644 index 0000000..50db0d2 --- /dev/null +++ b/coapthon/reverse_proxy/coap.py @@ -0,0 +1,477 @@ +import logging.config +import random +import socket +import struct +import threading +import xml.etree.ElementTree as ElementTree + +import os +import re + +from coapthon import defines +from coapthon.client.helperclient import HelperClient +from coapthon.layers.blocklayer import BlockLayer +from coapthon.layers.cachelayer import CacheLayer +from coapthon.layers.forwardLayer import ForwardLayer +from coapthon.layers.messagelayer import MessageLayer +from coapthon.layers.observelayer import ObserveLayer +from coapthon.layers.resourcelayer import ResourceLayer +from coapthon.messages.message import Message +from coapthon.messages.request import Request +from coapthon.resources.remoteResource import RemoteResource +from coapthon.resources.resource import Resource +from coapthon.serializer import Serializer +from coapthon.utils import Tree, create_logging + +__author__ = 'Giacomo Tanganelli' + + +if not os.path.isfile("logging.conf"): + create_logging() + +logger = logging.getLogger(__name__) +logging.config.fileConfig("logging.conf", disable_existing_loggers=False) + + +class CoAP(object): + """ + Implementation of the Reverse Proxy + """ + def __init__(self, server_address, xml_file, multicast=False, starting_mid=None, cache=False, sock=None): + """ + Initialize the Reverse Proxy. + + :param server_address: Server address for incoming connections + :param xml_file: the xml file that describe remote servers + :param multicast: if the ip is a multicast address + :param starting_mid: used for testing purposes + :param cache: if a cache must be used + :param sock: if a socket has been created externally, it can be used directly + """ + self.stopped = threading.Event() + self.stopped.clear() + self.to_be_stopped = [] + self.purge = threading.Thread(target=self.purge) + self.purge.start() + + self._messageLayer = MessageLayer(starting_mid) + self._blockLayer = BlockLayer() + self._observeLayer = ObserveLayer() + + self._forwardLayer = ForwardLayer(self) + self.resourceLayer = ResourceLayer(self) + self.cache_enable = cache + if self.cache_enable: + self._cacheLayer = CacheLayer(defines.REVERSE_PROXY) + else: + self._cacheLayer = None + + # Resource directory + root = Resource('root', self, visible=False, observable=False, allow_children=True) + root.path = '/' + self.root = Tree() + self.root["/"] = root + self._serializer = None + + self.server_address = server_address + self.multicast = multicast + self.file_xml = xml_file + self._mapping = {} + + addrinfo = socket.getaddrinfo(self.server_address[0], None)[0] + + if sock is not None: + + # Use given socket, could be a DTLS socket + self._socket = sock + + elif self.multicast: # pragma: no cover + + # Create a socket + # self._socket.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_TTL, 255) + # self._socket.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_LOOP, 1) + + # Join group + if addrinfo[0] == socket.AF_INET: # IPv4 + self._socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP) + + # Allow multiple copies of this program on one machine + # (not strictly needed) + self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + self._socket.bind((defines.ALL_COAP_NODES, self.server_address[1])) + mreq = struct.pack("4sl", socket.inet_aton(defines.ALL_COAP_NODES), socket.INADDR_ANY) + self._socket.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq) + self._unicast_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + self._unicast_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + self._unicast_socket.bind(self.server_address) + else: + self._socket = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM, socket.IPPROTO_UDP) + + # Allow multiple copies of this program on one machine + # (not strictly needed) + self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + self._socket.bind((defines.ALL_COAP_NODES_IPV6, self.server_address[1])) + + addrinfo_multicast = socket.getaddrinfo(defines.ALL_COAP_NODES_IPV6, 5683)[0] + group_bin = socket.inet_pton(socket.AF_INET6, addrinfo_multicast[4][0]) + mreq = group_bin + struct.pack('@I', 0) + self._socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_JOIN_GROUP, mreq) + self._unicast_socket = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM) + self._unicast_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + self._unicast_socket.bind(self.server_address) + else: + if addrinfo[0] == socket.AF_INET: # IPv4 + self._socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + else: + self._socket = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM) + self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + + self._socket.bind(self.server_address) + + self.parse_config() + + def parse_config(self): + """ + Parse the xml file with remote servers and discover resources on each found server. + """ + tree = ElementTree.parse(self.file_xml) + root = tree.getroot() + for server in root.findall('server'): + destination = server.text + name = server.get("name") + self.discover_remote(destination, name) + + def discover_remote(self, destination, name): + """ + Discover resources on remote servers. + + :param destination: the remote server (ip, port) + :type destination: tuple + :param name: the name of the remote server + :type name: String + """ + assert (isinstance(destination, str)) + if destination.startswith("["): + split = destination.split("]", 1) + host = split[0][1:] + port = int(split[1][1:]) + else: + split = destination.split(":", 1) + host = split[0] + port = int(split[1]) + server = (host, port) + client = HelperClient(server) + response = client.discover() + client.stop() + self.discover_remote_results(response, name) + + def discover_remote_results(self, response, name): + """ + Create a new remote server resource for each valid discover response. + + :param response: the response to the discovery request + :param name: the server name + """ + host, port = response.source + + if response.code == defines.Codes.CONTENT.number: + resource = Resource('server', self, visible=True, observable=False, allow_children=True) + self.add_resource(name, resource) + self._mapping[name] = (host, port) + self.parse_core_link_format(response.payload, name, (host, port)) + else: + logger.error("Server: " + response.source + " isn't valid.") + + def parse_core_link_format(self, link_format, base_path, remote_server): + """ + Parse discovery results. + + :param link_format: the payload of the response to the discovery request + :param base_path: the base path used to create child resources discovered on the remote server + :param remote_server: the (ip, port) of the remote server + """ + while len(link_format) > 0: + pattern = "<([^>]*)>;" + result = re.match(pattern, link_format) + path = result.group(1) + path = path.split("/") + path = path[1:][0] + link_format = link_format[result.end(1) + 2:] + pattern = "([^<,])*" + result = re.match(pattern, link_format) + attributes = result.group(0) + dict_att = {} + if len(attributes) > 0: + attributes = attributes.split(";") + for att in attributes: + a = att.split("=") + if len(a) > 1: + dict_att[a[0]] = a[1] + else: + dict_att[a[0]] = a[0] + link_format = link_format[result.end(0) + 1:] + # TODO handle observing + resource = RemoteResource('server', remote_server, path, coap_server=self, visible=True, observable=False, + allow_children=True) + resource.attributes = dict_att + self.add_resource(base_path + "/" + path, resource) + + logger.info(self.root.dump()) + + def purge(self): + """ + Clean old transactions + """ + while not self.stopped.isSet(): + self.stopped.wait(timeout=defines.EXCHANGE_LIFETIME) + self._messageLayer.purge() + + 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: + + self.receive_datagram((data, client_address)) + except RuntimeError: + logger.exception("Exception with Executor") + self._socket.close() + + def close(self): + """ + Stop the server. + + """ + logger.info("Stop server") + self.stopped.set() + for event in self.to_be_stopped: + event.set() + self._socket.close() + + def receive_datagram(self, args): + """ + Handle messages coming from the udp socket. + + :param args: (data, client_address) + """ + data, client_address = args + + serializer = Serializer() + message = serializer.deserialize(data, client_address) + if isinstance(message, int): + logger.error("receive_datagram - BAD REQUEST") + + rst = Message() + rst.destination = client_address + rst.type = defines.Types["RST"] + rst.code = message + self.send_datagram(rst) + return + logger.debug("receive_datagram - " + str(message)) + if isinstance(message, Request): + + transaction = self._messageLayer.receive_request(message) + + if transaction.request.duplicated and transaction.completed: + logger.debug("message duplicated,transaction completed") + transaction = self._observeLayer.send_response(transaction) + transaction = self._blockLayer.send_response(transaction) + transaction = self._messageLayer.send_response(transaction) + self.send_datagram(transaction.response) + return + elif transaction.request.duplicated and not transaction.completed: + logger.debug("message duplicated,transaction NOT completed") + self._send_ack(transaction) + return + + transaction.separate_timer = self._start_separate_timer(transaction) + + transaction = self._blockLayer.receive_request(transaction) + + if transaction.block_transfer: + self._stop_separate_timer(transaction.separate_timer) + transaction = self._messageLayer.send_response(transaction) + self.send_datagram(transaction.response) + return + + transaction = self._observeLayer.receive_request(transaction) + + """ + call to the cache layer to check if there's a cached response for the request + if not, call the forward layer + """ + if self._cacheLayer is not None: + transaction = self._cacheLayer.receive_request(transaction) + + if transaction.cacheHit is False: + logger.debug(transaction.request) + transaction = self._forwardLayer.receive_request_reverse(transaction) + logger.debug(transaction.response) + + transaction = self._observeLayer.send_response(transaction) + + transaction = self._blockLayer.send_response(transaction) + + transaction = self._cacheLayer.send_response(transaction) + else: + transaction = self._forwardLayer.receive_request_reverse(transaction) + + transaction = self._observeLayer.send_response(transaction) + + transaction = self._blockLayer.send_response(transaction) + + self._stop_separate_timer(transaction.separate_timer) + + transaction = self._messageLayer.send_response(transaction) + + if transaction.response is not None: + if transaction.response.type == defines.Types["CON"]: + self._start_retrasmission(transaction, transaction.response) + self.send_datagram(transaction.response) + + elif isinstance(message, Message): + transaction = self._messageLayer.receive_empty(message) + if transaction is not None: + transaction = self._blockLayer.receive_empty(message, transaction) + self._observeLayer.receive_empty(message, transaction) + + else: # pragma: no cover + logger.error("Received response from %s", message.source) + + def send_datagram(self, message): + """ + Send a message through the udp socket. + + :type message: Message + :param message: the message to send + """ + if not self.stopped.isSet(): + host, port = message.destination + logger.debug("send_datagram - " + str(message)) + serializer = Serializer() + message = serializer.serialize(message) + + self._socket.sendto(message, (host, port)) + + def add_resource(self, path, resource): + """" + Helper function to add resources to the resource directory during server initialization. + + :param path: the path for the new created resource + :type resource: Resource + :param resource: the resource to be added + """ + + 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 + + def _start_retrasmission(self, transaction, message): + """ + Start the retransmission task. + + :type transaction: Transaction + :param transaction: the transaction that owns the message that needs retransmission + :type message: Message + :param message: the message that needs the retransmission task + """ + with transaction: + if message.type == defines.Types['CON']: + future_time = random.uniform(defines.ACK_TIMEOUT, (defines.ACK_TIMEOUT * defines.ACK_RANDOM_FACTOR)) + transaction.retransmit_thread = threading.Thread(target=self._retransmit, + args=(transaction, message, future_time, 0)) + transaction.retransmit_stop = threading.Event() + self.to_be_stopped.append(transaction.retransmit_stop) + transaction.retransmit_thread.start() + + def _retransmit(self, transaction, message, future_time, retransmit_count): + """ + Thread function to retransmit the message in the future + + :param transaction: the transaction that owns the message that needs retransmission + :param message: the message that needs the retransmission task + :param future_time: the amount of time to wait before a new attempt + :param retransmit_count: the number of retransmissions + """ + with transaction: + while retransmit_count < defines.MAX_RETRANSMIT and (not message.acknowledged and not message.rejected) \ + and not self.stopped.isSet(): + transaction.retransmit_stop.wait(timeout=future_time) + if not message.acknowledged and not message.rejected and not self.stopped.isSet(): + retransmit_count += 1 + future_time *= 2 + self.send_datagram(message) + + if message.acknowledged or message.rejected: + message.timeouted = False + else: + logger.warning("Give up on message {message}".format(message=message.line_print)) + message.timeouted = True + if message.observe is not None: + self._observeLayer.remove_subscriber(message) + + try: + self.to_be_stopped.remove(transaction.retransmit_stop) + except ValueError: + pass + transaction.retransmit_stop = None + transaction.retransmit_thread = None + + def _start_separate_timer(self, transaction): + """ + Start a thread to handle separate mode. + + :type transaction: Transaction + :param transaction: the transaction that is in processing + :rtype : the Timer object + """ + t = threading.Timer(defines.ACK_TIMEOUT, self._send_ack, (transaction,)) + t.start() + return t + + @staticmethod + def _stop_separate_timer(timer): + """ + Stop the separate Thread if an answer has been already provided to the client. + + :param timer: The Timer object + """ + timer.cancel() + + def _send_ack(self, transaction): + """ + Sends an ACK message for the request. + + :param transaction: the transaction that owns the request + """ + + ack = Message() + ack.type = defines.Types['ACK'] + + if not transaction.request.acknowledged: + ack = self._messageLayer.send_empty(transaction, transaction.request, ack) + self.send_datagram(ack) diff --git a/coapthon/serializer.py b/coapthon/serializer.py new file mode 100644 index 0000000..3539587 --- /dev/null +++ b/coapthon/serializer.py @@ -0,0 +1,408 @@ +import logging +import struct +import ctypes +from coapthon.messages.request import Request +from coapthon.messages.response import Response +from coapthon.messages.option import Option +from coapthon import defines +from coapthon.messages.message import Message + +__author__ = 'Giacomo Tanganelli' + +logger = logging.getLogger(__name__) + + +class Serializer(object): + """ + Serializer class to serialize and deserialize CoAP message to/from udp streams. + """ + @staticmethod + def deserialize(datagram, source): + """ + De-serialize a stream of byte to a message. + + :param datagram: the incoming udp message + :param source: the source address and port (ip, port) + :return: the message + :rtype: Message + """ + try: + fmt = "!BBH" + pos = struct.calcsize(fmt) + s = struct.Struct(fmt) + values = s.unpack_from(datagram) + first = values[0] + code = values[1] + mid = values[2] + version = (first & 0xC0) >> 6 + message_type = (first & 0x30) >> 4 + token_length = (first & 0x0F) + if Serializer.is_response(code): + message = Response() + message.code = code + elif Serializer.is_request(code): + message = Request() + message.code = code + else: + message = Message() + message.source = source + message.destination = None + message.version = version + message.type = message_type + message.mid = mid + if token_length > 0: + fmt = "%ss" % token_length + s = struct.Struct(fmt) + token_value = s.unpack_from(datagram[pos:])[0] + message.token = token_value.decode("utf-8") + else: + message.token = None + + pos += token_length + current_option = 0 + values = datagram[pos:] + length_packet = len(values) + pos = 0 + while pos < length_packet: + next_byte = struct.unpack("B", values[pos].to_bytes(1, "big"))[0] + pos += 1 + if next_byte != int(defines.PAYLOAD_MARKER): + # the first 4 bits of the byte represent the option delta + # delta = self._reader.read(4).uint + num, option_length, pos = Serializer.read_option_value_len_from_byte(next_byte, pos, values) + current_option += num + # read option + try: + option_item = defines.OptionRegistry.LIST[current_option] + except KeyError: + (opt_critical, _, _) = defines.OptionRegistry.get_option_flags(current_option) + if opt_critical: + raise AttributeError("Critical option %s unknown" % current_option) + else: + # If the non-critical option is unknown + # (vendor-specific, proprietary) - just skip it + #log.err("unrecognized option %d" % current_option) + pass + else: + if option_length == 0: + value = None + elif option_item.value_type == defines.INTEGER: + tmp = values[pos: pos + option_length] + value = 0 + for b in tmp: + value = (value << 8) | struct.unpack("B", b.to_bytes(1, "big"))[0] + elif option_item.value_type == defines.OPAQUE: + tmp = values[pos: pos + option_length] + value = tmp + else: + value = values[pos: pos + option_length] + + option = Option() + option.number = current_option + option.value = Serializer.convert_to_raw(current_option, value, option_length) + + message.add_option(option) + if option.number == defines.OptionRegistry.CONTENT_TYPE.number: + message.payload_type = option.value + finally: + pos += option_length + else: + + if length_packet <= pos: + # log.err("Payload Marker with no payload") + raise AttributeError("Packet length %s, pos %s" % (length_packet, pos)) + message.payload = "" + payload = values[pos:] + message.payload = payload.decode("utf-8") + pos += len(payload) + + return message + except AttributeError: + return defines.Codes.BAD_REQUEST.number + except struct.error: + return defines.Codes.BAD_REQUEST.number + + @staticmethod + def serialize(message): + """ + Serialize a message to a udp packet + + :type message: Message + :param message: the message to be serialized + :rtype: stream of byte + :return: the message serialized + """ + fmt = "!BBH" + + if message.token is None or message.token == "": + tkl = 0 + else: + tkl = len(message.token) + tmp = (defines.VERSION << 2) + tmp |= message.type + tmp <<= 4 + tmp |= tkl + + values = [tmp, message.code, message.mid] + + if message.token is not None and tkl > 0: + + for b in str(message.token): + fmt += "c" + values.append(bytes(b, "utf-8")) + + options = Serializer.as_sorted_list(message.options) # already sorted + lastoptionnumber = 0 + for option in options: + + # write 4-bit option delta + optiondelta = option.number - lastoptionnumber + optiondeltanibble = Serializer.get_option_nibble(optiondelta) + tmp = (optiondeltanibble << defines.OPTION_DELTA_BITS) + + # write 4-bit option length + optionlength = option.length + optionlengthnibble = Serializer.get_option_nibble(optionlength) + tmp |= optionlengthnibble + fmt += "B" + values.append(tmp) + + # write extended option delta field (0 - 2 bytes) + if optiondeltanibble == 13: + fmt += "B" + values.append(optiondelta - 13) + elif optiondeltanibble == 14: + fmt += "H" + values.append(optiondelta - 269) + + # write extended option length field (0 - 2 bytes) + if optionlengthnibble == 13: + fmt += "B" + values.append(optionlength - 13) + elif optionlengthnibble == 14: + fmt += "H" + values.append(optionlength - 269) + + # write option value + if optionlength > 0: + opt_type = defines.OptionRegistry.LIST[option.number].value_type + if opt_type == defines.INTEGER: + words = Serializer.int_to_words(option.value, optionlength, 8) + for num in range(0, optionlength): + fmt += "B" + values.append(words[num]) + elif opt_type == defines.STRING: + fmt += str(len(bytes(option.value, "utf-8"))) + "s" + values.append(bytes(option.value, "utf-8")) + + else: # OPAQUE + for b in option.value: + fmt += "B" + values.append(b) + + # update last option number + lastoptionnumber = option.number + + payload = message.payload + + if payload is not None and len(payload) > 0: + # if payload is present and of non-zero length, it is prefixed by + # an one-byte Payload Marker (0xFF) which indicates the end of + # options and the start of the payload + + fmt += "B" + values.append(defines.PAYLOAD_MARKER) + + fmt += str(len(bytes(payload, "utf-8"))) + "s" + values.append(bytes(payload, "utf-8")) + # for b in str(payload): + # fmt += "c" + # values.append(bytes(b, "utf-8")) + + datagram = None + if values[1] is None: + values[1] = 0 + if values[2] is None: + values[2] = 0 + try: + s = struct.Struct(fmt) + datagram = ctypes.create_string_buffer(s.size) + s.pack_into(datagram, 0, *values) + except struct.error: + # The .exception method will report on the exception encountered + # and provide a traceback. + logger.debug(fmt) + logger.debug(values) + logging.exception('Failed to pack structure') + + return datagram + + @staticmethod + def is_request(code): + """ + Checks if is request. + + :return: True, if is request + """ + return defines.REQUEST_CODE_LOWER_BOUND <= code <= defines.REQUEST_CODE_UPPER_BOUND + + @staticmethod + def is_response(code): + """ + Checks if is response. + + :return: True, if is response + """ + return defines.RESPONSE_CODE_LOWER_BOUND <= code <= defines.RESPONSE_CODE_UPPER_BOUND + + @staticmethod + def read_option_value_from_nibble(nibble, pos, values): + """ + Calculates the value used in the extended option fields. + + :param nibble: the 4-bit option header value. + :return: the value calculated from the nibble and the extended option value. + """ + if nibble <= 12: + return nibble, pos + elif nibble == 13: + tmp = struct.unpack("!B", values[pos].to_bytes(1, "big"))[0] + 13 + pos += 1 + return tmp, pos + elif nibble == 14: + s = struct.Struct("!H") + tmp = s.unpack_from(values[pos:].to_bytes(2, "big"))[0] + 269 + pos += 2 + return tmp, pos + else: + raise AttributeError("Unsupported option nibble " + str(nibble)) + + @staticmethod + def read_option_value_len_from_byte(byte, pos, values): + """ + Calculates the value and length used in the extended option fields. + + :param byte: 1-byte option header value. + :return: the value and length, calculated from the header including the extended fields. + """ + h_nibble = (byte & 0xF0) >> 4 + l_nibble = byte & 0x0F + value = 0 + length = 0 + if h_nibble <= 12: + value = h_nibble + elif h_nibble == 13: + value = struct.unpack("!B", values[pos].to_bytes(1, "big"))[0] + 13 + pos += 1 + elif h_nibble == 14: + s = struct.Struct("!H") + value = s.unpack_from(values[pos:].to_bytes(2, "big"))[0] + 269 + pos += 2 + else: + raise AttributeError("Unsupported option number nibble " + str(h_nibble)) + + if l_nibble <= 12: + length = l_nibble + elif l_nibble == 13: + length = struct.unpack("!B", values[pos].to_bytes(1, "big"))[0] + 13 + pos += 1 + elif l_nibble == 14: + length = s.unpack_from(values[pos:].to_bytes(2, "big"))[0] + 269 + pos += 2 + else: + raise AttributeError("Unsupported option length nibble " + str(l_nibble)) + return value, length, pos + + @staticmethod + def convert_to_raw(number, value, length): + """ + Get the value of an option as a ByteArray. + + :param number: the option number + :param value: the option value + :param length: the option length + :return: the value of an option as a BitArray + """ + + opt_type = defines.OptionRegistry.LIST[number].value_type + + if length == 0 and opt_type != defines.INTEGER: + return bytes() + elif length == 0 and opt_type == defines.INTEGER: + return 0 + elif opt_type == defines.STRING: + if isinstance(value, bytes): + return value.decode("utf-8") + elif opt_type == defines.OPAQUE: + if isinstance(value, bytes): + return value + else: + return bytes(value, "utf-8") + if isinstance(value, tuple): + value = value[0] + if isinstance(value, str): + value = str(value) + if isinstance(value, str): + return bytearray(value, "utf-8") + elif isinstance(value, int): + return value + else: + return bytearray(value) + + @staticmethod + def as_sorted_list(options): + """ + Returns all options in a list sorted according to their option numbers. + + :return: the sorted list + """ + if len(options) > 0: + options = sorted(options, key=lambda o: o.number) + return options + + @staticmethod + def get_option_nibble(optionvalue): + """ + Returns the 4-bit option header value. + + :param optionvalue: the option value (delta or length) to be encoded. + :return: the 4-bit option header value. + """ + if optionvalue <= 12: + return optionvalue + elif optionvalue <= 255 + 13: + return 13 + elif optionvalue <= 65535 + 269: + return 14 + else: + raise AttributeError("Unsupported option delta " + optionvalue) + + @staticmethod + def int_to_words(int_val, num_words=4, word_size=32): + """ + Convert a int value to bytes. + + :param int_val: an arbitrary length Python integer to be split up. + Network byte order is assumed. Raises an IndexError if width of + integer (in bits) exceeds word_size * num_words. + + :param num_words: number of words expected in return value tuple. + + :param word_size: size/width of individual words (in bits). + + :return: a list of fixed width words based on provided parameters. + """ + max_int = 2 ** (word_size*num_words) - 1 + max_word_size = 2 ** word_size - 1 + + if not 0 <= int_val <= max_int: + raise AttributeError('integer %r is out of bounds!' % hex(int_val)) + + words = [] + for _ in range(num_words): + word = int_val & max_word_size + words.append(int(word)) + int_val >>= word_size + words.reverse() + + return words diff --git a/coapthon/serializer.py.bak b/coapthon/serializer.py.bak new file mode 100644 index 0000000..1c094ce --- /dev/null +++ b/coapthon/serializer.py.bak @@ -0,0 +1,397 @@ +import logging +import struct +import ctypes +from coapthon.messages.request import Request +from coapthon.messages.response import Response +from coapthon.messages.option import Option +from coapthon import defines +from coapthon.messages.message import Message + +__author__ = 'Giacomo Tanganelli' + +logger = logging.getLogger(__name__) + + +class Serializer(object): + """ + Serializer class to serialize and deserialize CoAP message to/from udp streams. + """ + @staticmethod + def deserialize(datagram, source): + """ + De-serialize a stream of byte to a message. + + :param datagram: the incoming udp message + :param source: the source address and port (ip, port) + :return: the message + :rtype: Message + """ + try: + fmt = "!BBH" + pos = struct.calcsize(fmt) + s = struct.Struct(fmt) + values = s.unpack_from(datagram) + first = values[0] + code = values[1] + mid = values[2] + version = (first & 0xC0) >> 6 + message_type = (first & 0x30) >> 4 + token_length = (first & 0x0F) + if Serializer.is_response(code): + message = Response() + message.code = code + elif Serializer.is_request(code): + message = Request() + message.code = code + else: + message = Message() + message.source = source + message.destination = None + message.version = version + message.type = message_type + message.mid = mid + if token_length > 0: + fmt = "%ss" % token_length + s = struct.Struct(fmt) + token_value = s.unpack_from(datagram[pos:])[0] + message.token = token_value + else: + message.token = None + + pos += token_length + current_option = 0 + values = datagram[pos:] + length_packet = len(values) + pos = 0 + while pos < length_packet: + next_byte = struct.unpack("B", values[pos])[0] + pos += 1 + if next_byte != int(defines.PAYLOAD_MARKER): + # the first 4 bits of the byte represent the option delta + # delta = self._reader.read(4).uint + num, option_length, pos = Serializer.read_option_value_len_from_byte(next_byte, pos, values) + current_option += num + # read option + try: + option_item = defines.OptionRegistry.LIST[current_option] + except KeyError: + (opt_critical, _, _) = defines.OptionRegistry.get_option_flags(current_option) + if opt_critical: + raise AttributeError("Critical option %s unknown" % current_option) + else: + # If the non-critical option is unknown + # (vendor-specific, proprietary) - just skip it + #log.err("unrecognized option %d" % current_option) + pass + else: + if option_length == 0: + value = None + elif option_item.value_type == defines.INTEGER: + tmp = values[pos: pos + option_length] + value = 0 + for b in tmp: + value = (value << 8) | struct.unpack("B", b)[0] + elif option_item.value_type == defines.OPAQUE: + tmp = values[pos: pos + option_length] + value = bytearray(tmp) + else: + tmp = values[pos: pos + option_length] + value = "" + for b in tmp: + value += str(b) + + option = Option() + option.number = current_option + option.value = Serializer.convert_to_raw(current_option, value, option_length) + + message.add_option(option) + if option.number == defines.OptionRegistry.CONTENT_TYPE.number: + message.payload_type = option.value + finally: + pos += option_length + else: + + if length_packet <= pos: + # log.err("Payload Marker with no payload") + raise AttributeError("Packet length %s, pos %s" % (length_packet, pos)) + message.payload = "" + payload = values[pos:] + for b in payload: + message.payload += str(b) + pos += 1 + return message + except AttributeError: + return defines.Codes.BAD_REQUEST.number + except struct.error: + return defines.Codes.BAD_REQUEST.number + + @staticmethod + def serialize(message): + """ + Serialize a message to a udp packet + + :type message: Message + :param message: the message to be serialized + :rtype: stream of byte + :return: the message serialized + """ + fmt = "!BBH" + + if message.token is None or message.token == "": + tkl = 0 + else: + tkl = len(message.token) + tmp = (defines.VERSION << 2) + tmp |= message.type + tmp <<= 4 + tmp |= tkl + values = [tmp, message.code, message.mid] + + if message.token is not None and tkl > 0: + + for b in str(message.token): + fmt += "c" + values.append(b) + + options = Serializer.as_sorted_list(message.options) # already sorted + lastoptionnumber = 0 + for option in options: + + # write 4-bit option delta + optiondelta = option.number - lastoptionnumber + optiondeltanibble = Serializer.get_option_nibble(optiondelta) + tmp = (optiondeltanibble << defines.OPTION_DELTA_BITS) + + # write 4-bit option length + optionlength = option.length + optionlengthnibble = Serializer.get_option_nibble(optionlength) + tmp |= optionlengthnibble + fmt += "B" + values.append(tmp) + + # write extended option delta field (0 - 2 bytes) + if optiondeltanibble == 13: + fmt += "B" + values.append(optiondelta - 13) + elif optiondeltanibble == 14: + fmt += "H" + values.append(optiondelta - 269) + + # write extended option length field (0 - 2 bytes) + if optionlengthnibble == 13: + fmt += "B" + values.append(optionlength - 13) + elif optionlengthnibble == 14: + fmt += "H" + values.append(optionlength - 269) + + # write option value + if optionlength > 0: + opt_type = defines.OptionRegistry.LIST[option.number].value_type + if opt_type == defines.INTEGER: + words = Serializer.int_to_words(option.value, optionlength, 8) + for num in range(0, optionlength): + fmt += "B" + values.append(words[num]) + elif opt_type == defines.STRING: + for b in str(option.value): + fmt += "c" + values.append(b) + else: + for b in option.value: + fmt += "B" + values.append(b) + + + # update last option number + lastoptionnumber = option.number + + payload = message.payload + + if payload is not None and len(payload) > 0: + # if payload is present and of non-zero length, it is prefixed by + # an one-byte Payload Marker (0xFF) which indicates the end of + # options and the start of the payload + + fmt += "B" + values.append(defines.PAYLOAD_MARKER) + + for b in str(payload): + fmt += "c" + values.append(b) + + datagram = None + if values[1] is None: + values[1] = 0 + try: + s = struct.Struct(fmt) + datagram = ctypes.create_string_buffer(s.size) + s.pack_into(datagram, 0, *values) + except struct.error: + # The .exception method will report on the exception encountered + # and provide a traceback. + logging.exception('Failed to pack structure') + + return datagram + + @staticmethod + def is_request(code): + """ + Checks if is request. + + :return: True, if is request + """ + return defines.REQUEST_CODE_LOWER_BOUND <= code <= defines.REQUEST_CODE_UPPER_BOUND + + @staticmethod + def is_response(code): + """ + Checks if is response. + + :return: True, if is response + """ + return defines.RESPONSE_CODE_LOWER_BOUND <= code <= defines.RESPONSE_CODE_UPPER_BOUND + + @staticmethod + def read_option_value_from_nibble(nibble, pos, values): + """ + Calculates the value used in the extended option fields. + + :param nibble: the 4-bit option header value. + :return: the value calculated from the nibble and the extended option value. + """ + if nibble <= 12: + return nibble, pos + elif nibble == 13: + tmp = struct.unpack("!B", values[pos])[0] + 13 + pos += 1 + return tmp, pos + elif nibble == 14: + s = struct.Struct("!H") + tmp = s.unpack_from(values[pos:])[0] + 269 + pos += 2 + return tmp, pos + else: + raise AttributeError("Unsupported option nibble " + str(nibble)) + + @staticmethod + def read_option_value_len_from_byte(byte, pos, values): + """ + Calculates the value and length used in the extended option fields. + + :param byte: 1-byte option header value. + :return: the value and length, calculated from the header including the extended fields. + """ + h_nibble = (byte & 0xF0) >> 4 + l_nibble = byte & 0x0F + value = 0 + length = 0 + if h_nibble <= 12: + value = h_nibble + elif h_nibble == 13: + value = struct.unpack("!B", values[pos])[0] + 13 + pos += 1 + elif h_nibble == 14: + s = struct.Struct("!H") + value = s.unpack_from(values[pos:])[0] + 269 + pos += 2 + else: + raise AttributeError("Unsupported option number nibble " + str(h_nibble)) + + if l_nibble <= 12: + length = l_nibble + elif l_nibble == 13: + length = struct.unpack("!B", values[pos])[0] + 13 + pos += 1 + elif l_nibble == 14: + length = s.unpack_from(values[pos:])[0] + 269 + pos += 2 + else: + raise AttributeError("Unsupported option length nibble " + str(l_nibble)) + return value, length, pos + + @staticmethod + def convert_to_raw(number, value, length): + """ + Get the value of an option as a ByteArray. + + :param number: the option number + :param value: the option value + :param length: the option length + :return: the value of an option as a BitArray + """ + + opt_type = defines.OptionRegistry.LIST[number].value_type + + if length == 0 and opt_type != defines.INTEGER: + return bytearray() + if length == 0 and opt_type == defines.INTEGER: + return 0 + if isinstance(value, tuple): + value = value[0] + if isinstance(value, unicode): + value = str(value) + if isinstance(value, str): + return bytearray(value, "utf-8") + elif isinstance(value, int): + return value + else: + return bytearray(value) + + @staticmethod + def as_sorted_list(options): + """ + Returns all options in a list sorted according to their option numbers. + + :return: the sorted list + """ + if len(options) > 0: + options.sort(None, key=lambda o: o.number) + return options + + @staticmethod + def get_option_nibble(optionvalue): + """ + Returns the 4-bit option header value. + + :param optionvalue: the option value (delta or length) to be encoded. + :return: the 4-bit option header value. + """ + if optionvalue <= 12: + return optionvalue + elif optionvalue <= 255 + 13: + return 13 + elif optionvalue <= 65535 + 269: + return 14 + else: + raise AttributeError("Unsupported option delta " + optionvalue) + + @staticmethod + def int_to_words(int_val, num_words=4, word_size=32): + """ + Convert a int value to bytes. + + :param int_val: an arbitrary length Python integer to be split up. + Network byte order is assumed. Raises an IndexError if width of + integer (in bits) exceeds word_size * num_words. + + :param num_words: number of words expected in return value tuple. + + :param word_size: size/width of individual words (in bits). + + :return: a list of fixed width words based on provided parameters. + """ + max_int = 2 ** (word_size*num_words) - 1 + max_word_size = 2 ** word_size - 1 + + if not 0 <= int_val <= max_int: + raise AttributeError('integer %r is out of bounds!' % hex(int_val)) + + words = [] + for _ in range(num_words): + word = int_val & max_word_size + words.append(int(word)) + int_val >>= word_size + words.reverse() + + return words diff --git a/coapthon/server/__init__.py b/coapthon/server/__init__.py new file mode 100644 index 0000000..f89a37d --- /dev/null +++ b/coapthon/server/__init__.py @@ -0,0 +1 @@ +__author__ = 'Giacomo Tanganelli' \ No newline at end of file diff --git a/coapthon/server/coap.py b/coapthon/server/coap.py new file mode 100644 index 0000000..e2b4e5e --- /dev/null +++ b/coapthon/server/coap.py @@ -0,0 +1,422 @@ +import logging.config +import os +import random +import socket +import struct +import threading + +from coapthon import defines +from coapthon.layers.blocklayer import BlockLayer +from coapthon.layers.messagelayer import MessageLayer +from coapthon.layers.observelayer import ObserveLayer +from coapthon.layers.requestlayer import RequestLayer +from coapthon.layers.resourcelayer import ResourceLayer +from coapthon.messages.message import Message +from coapthon.messages.request import Request +from coapthon.messages.response import Response +from coapthon.resources.resource import Resource +from coapthon.serializer import Serializer +from coapthon.utils import Tree +from coapthon.utils import create_logging +import collections + + +__author__ = 'Giacomo Tanganelli' + + +if not os.path.isfile("logging.conf"): + create_logging() + +logger = logging.getLogger(__name__) +logging.config.fileConfig("logging.conf", disable_existing_loggers=False) + + +class CoAP(object): + """ + Implementation of the CoAP server + """ + def __init__(self, server_address, multicast=False, starting_mid=None, sock=None, cb_ignore_listen_exception=None): + """ + Initialize the server. + + :param server_address: Server address for incoming connections + :param multicast: if the ip is a multicast address + :param starting_mid: used for testing purposes + :param sock: if a socket has been created externally, it can be used directly + :param cb_ignore_listen_exception: Callback function to handle exception raised during the socket listen operation + """ + self.stopped = threading.Event() + self.stopped.clear() + self.to_be_stopped = [] + self.purge = threading.Thread(target=self.purge) + self.purge.start() + + self._messageLayer = MessageLayer(starting_mid) + self._blockLayer = BlockLayer() + self._observeLayer = ObserveLayer() + self._requestLayer = RequestLayer(self) + self.resourceLayer = ResourceLayer(self) + + # Resource directory + root = Resource('root', self, visible=False, observable=False, allow_children=False) + root.path = '/' + self.root = Tree() + self.root["/"] = root + self._serializer = None + + self.server_address = server_address + self.multicast = multicast + self._cb_ignore_listen_exception = cb_ignore_listen_exception + + addrinfo = socket.getaddrinfo(self.server_address[0], None)[0] + + if sock is not None: + + # Use given socket, could be a DTLS socket + self._socket = sock + + elif self.multicast: # pragma: no cover + + # Create a socket + # self._socket.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_TTL, 255) + # self._socket.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_LOOP, 1) + + # Join group + if addrinfo[0] == socket.AF_INET: # IPv4 + self._socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP) + + # Allow multiple copies of this program on one machine + # (not strictly needed) + self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + self._socket.bind((defines.ALL_COAP_NODES, self.server_address[1])) + mreq = struct.pack("4sl", socket.inet_aton(defines.ALL_COAP_NODES), socket.INADDR_ANY) + self._socket.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq) + self._unicast_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + self._unicast_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + self._unicast_socket.bind(self.server_address) + else: + self._socket = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM, socket.IPPROTO_UDP) + + # Allow multiple copies of this program on one machine + # (not strictly needed) + self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + self._socket.bind((defines.ALL_COAP_NODES_IPV6, self.server_address[1])) + + addrinfo_multicast = socket.getaddrinfo(defines.ALL_COAP_NODES_IPV6, 5683)[0] + group_bin = socket.inet_pton(socket.AF_INET6, addrinfo_multicast[4][0]) + mreq = group_bin + struct.pack('@I', 0) + self._socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_JOIN_GROUP, mreq) + self._unicast_socket = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM) + self._unicast_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + self._unicast_socket.bind(self.server_address) + + else: + if addrinfo[0] == socket.AF_INET: # IPv4 + self._socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + else: + self._socket = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM) + self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + + self._socket.bind(self.server_address) + + def purge(self): + """ + Clean old transactions + + """ + while not self.stopped.isSet(): + self.stopped.wait(timeout=defines.EXCHANGE_LIFETIME) + self._messageLayer.purge() + + 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) + if len(client_address) > 2: + client_address = (client_address[0], client_address[1]) + except socket.timeout: + continue + except Exception as e: + if self._cb_ignore_listen_exception is not None and isinstance(self._cb_ignore_listen_exception, collections.Callable): + if self._cb_ignore_listen_exception(e, self): + continue + raise + try: + serializer = Serializer() + message = serializer.deserialize(data, client_address) + if isinstance(message, int): + logger.error("receive_datagram - BAD REQUEST") + + rst = Message() + rst.destination = client_address + rst.type = defines.Types["RST"] + rst.code = message + rst.mid = self._messageLayer.fetch_mid() + self.send_datagram(rst) + continue + + logger.debug("receive_datagram - " + str(message)) + if isinstance(message, Request): + transaction = self._messageLayer.receive_request(message) + if transaction.request.duplicated and transaction.completed: + logger.debug("message duplicated, transaction completed") + if transaction.response is not None: + self.send_datagram(transaction.response) + continue + elif transaction.request.duplicated and not transaction.completed: + logger.debug("message duplicated, transaction NOT completed") + self._send_ack(transaction) + continue + args = (transaction, ) + t = threading.Thread(target=self.receive_request, args=args) + t.start() + # self.receive_datagram(data, client_address) + elif isinstance(message, Response): + logger.error("Received response from %s", message.source) + + else: # is Message + transaction = self._messageLayer.receive_empty(message) + if transaction is not None: + with transaction: + self._blockLayer.receive_empty(message, transaction) + self._observeLayer.receive_empty(message, transaction) + + except RuntimeError: + logger.exception("Exception with Executor") + self._socket.close() + + def close(self): + """ + Stop the server. + + """ + logger.info("Stop server") + self.stopped.set() + for event in self.to_be_stopped: + event.set() + + def receive_request(self, transaction): + """ + Handle requests coming from the udp socket. + + :param transaction: the transaction created to manage the request + """ + + with transaction: + + transaction.separate_timer = self._start_separate_timer(transaction) + + self._blockLayer.receive_request(transaction) + + if transaction.block_transfer: + self._stop_separate_timer(transaction.separate_timer) + self._messageLayer.send_response(transaction) + self.send_datagram(transaction.response) + return + + self._observeLayer.receive_request(transaction) + + self._requestLayer.receive_request(transaction) + + if transaction.resource is not None and transaction.resource.changed: + self.notify(transaction.resource) + transaction.resource.changed = False + elif transaction.resource is not None and transaction.resource.deleted: + self.notify(transaction.resource) + transaction.resource.deleted = False + + self._observeLayer.send_response(transaction) + + self._blockLayer.send_response(transaction) + + self._stop_separate_timer(transaction.separate_timer) + + self._messageLayer.send_response(transaction) + + if transaction.response is not None: + if transaction.response.type == defines.Types["CON"]: + self._start_retransmission(transaction, transaction.response) + self.send_datagram(transaction.response) + + def send_datagram(self, message): + """ + Send a message through the udp socket. + + :type message: Message + :param message: the message to send + """ + if not self.stopped.isSet(): + host, port = message.destination + logger.debug("send_datagram - " + str(message)) + serializer = Serializer() + message = serializer.serialize(message) + if self.multicast: + self._unicast_socket.sendto(message, (host, port)) + else: + self._socket.sendto(message, (host, port)) + + def add_resource(self, path, resource): + """ + Helper function to add resources to the resource directory during server initialization. + + :param path: the path for the new created resource + :type resource: Resource + :param resource: the resource to be added + """ + + 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 + + def remove_resource(self, path): + """ + Helper function to remove resources. + + :param path: the path for the unwanted resource + :rtype : the removed object + """ + + 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 not None: + del(self.root[actual_path]) + return res + + def _start_retransmission(self, transaction, message): + """ + Start the retransmission task. + + :type transaction: Transaction + :param transaction: the transaction that owns the message that needs retransmission + :type message: Message + :param message: the message that needs the retransmission task + """ + with transaction: + if message.type == defines.Types['CON']: + future_time = random.uniform(defines.ACK_TIMEOUT, (defines.ACK_TIMEOUT * defines.ACK_RANDOM_FACTOR)) + transaction.retransmit_thread = threading.Thread(target=self._retransmit, + args=(transaction, message, future_time, 0)) + transaction.retransmit_stop = threading.Event() + self.to_be_stopped.append(transaction.retransmit_stop) + transaction.retransmit_thread.start() + + def _retransmit(self, transaction, message, future_time, retransmit_count): + """ + Thread function to retransmit the message in the future + + :param transaction: the transaction that owns the message that needs retransmission + :param message: the message that needs the retransmission task + :param future_time: the amount of time to wait before a new attempt + :param retransmit_count: the number of retransmissions + """ + with transaction: + while retransmit_count < defines.MAX_RETRANSMIT and (not message.acknowledged and not message.rejected) \ + and not self.stopped.isSet(): + if transaction.retransmit_stop is not None: + transaction.retransmit_stop.wait(timeout=future_time) + if not message.acknowledged and not message.rejected and not self.stopped.isSet(): + retransmit_count += 1 + future_time *= 2 + self.send_datagram(message) + + if message.acknowledged or message.rejected: + message.timeouted = False + else: + logger.warning("Give up on message {message}".format(message=message.line_print)) + message.timeouted = True + if message.observe is not None: + self._observeLayer.remove_subscriber(message) + + try: + self.to_be_stopped.remove(transaction.retransmit_stop) + except ValueError: + pass + transaction.retransmit_stop = None + transaction.retransmit_thread = None + + def _start_separate_timer(self, transaction): + """ + Start a thread to handle separate mode. + + :type transaction: Transaction + :param transaction: the transaction that is in processing + :rtype : the Timer object + """ + t = threading.Timer(defines.ACK_TIMEOUT, self._send_ack, (transaction,)) + t.start() + return t + + @staticmethod + def _stop_separate_timer(timer): + """ + Stop the separate Thread if an answer has been already provided to the client. + + :param timer: The Timer object + """ + timer.cancel() + + def _send_ack(self, transaction): + """ + Sends an ACK message for the request. + + :param transaction: the transaction that owns the request + """ + + ack = Message() + ack.type = defines.Types['ACK'] + # TODO handle mutex on transaction + if not transaction.request.acknowledged and transaction.request.type == defines.Types["CON"]: + ack = self._messageLayer.send_empty(transaction, transaction.request, ack) + self.send_datagram(ack) + + def notify(self, resource): + """ + Notifies the observers of a certain resource. + + :param resource: the resource + """ + observers = self._observeLayer.notify(resource) + logger.debug("Notify") + for transaction in observers: + with transaction: + transaction.response = None + transaction = self._requestLayer.receive_request(transaction) + transaction = self._observeLayer.send_response(transaction) + transaction = self._blockLayer.send_response(transaction) + transaction = self._messageLayer.send_response(transaction) + if transaction.response is not None: + if transaction.response.type == defines.Types["CON"]: + self._start_retransmission(transaction, transaction.response) + + self.send_datagram(transaction.response) diff --git a/coapthon/server/coap.py.bak b/coapthon/server/coap.py.bak new file mode 100644 index 0000000..c4db32f --- /dev/null +++ b/coapthon/server/coap.py.bak @@ -0,0 +1,421 @@ +import logging.config +import os +import random +import socket +import struct +import threading + +from coapthon import defines +from coapthon.layers.blocklayer import BlockLayer +from coapthon.layers.messagelayer import MessageLayer +from coapthon.layers.observelayer import ObserveLayer +from coapthon.layers.requestlayer import RequestLayer +from coapthon.layers.resourcelayer import ResourceLayer +from coapthon.messages.message import Message +from coapthon.messages.request import Request +from coapthon.messages.response import Response +from coapthon.resources.resource import Resource +from coapthon.serializer import Serializer +from coapthon.utils import Tree +from coapthon.utils import create_logging + + +__author__ = 'Giacomo Tanganelli' + + +if not os.path.isfile("logging.conf"): + create_logging() + +logger = logging.getLogger(__name__) +logging.config.fileConfig("logging.conf", disable_existing_loggers=False) + + +class CoAP(object): + """ + Implementation of the CoAP server + """ + def __init__(self, server_address, multicast=False, starting_mid=None, sock=None, cb_ignore_listen_exception=None): + """ + Initialize the server. + + :param server_address: Server address for incoming connections + :param multicast: if the ip is a multicast address + :param starting_mid: used for testing purposes + :param sock: if a socket has been created externally, it can be used directly + :param cb_ignore_listen_exception: Callback function to handle exception raised during the socket listen operation + """ + self.stopped = threading.Event() + self.stopped.clear() + self.to_be_stopped = [] + self.purge = threading.Thread(target=self.purge) + self.purge.start() + + self._messageLayer = MessageLayer(starting_mid) + self._blockLayer = BlockLayer() + self._observeLayer = ObserveLayer() + self._requestLayer = RequestLayer(self) + self.resourceLayer = ResourceLayer(self) + + # Resource directory + root = Resource('root', self, visible=False, observable=False, allow_children=False) + root.path = '/' + self.root = Tree() + self.root["/"] = root + self._serializer = None + + self.server_address = server_address + self.multicast = multicast + self._cb_ignore_listen_exception = cb_ignore_listen_exception + + addrinfo = socket.getaddrinfo(self.server_address[0], None)[0] + + if sock is not None: + + # Use given socket, could be a DTLS socket + self._socket = sock + + elif self.multicast: # pragma: no cover + + # Create a socket + # self._socket.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_TTL, 255) + # self._socket.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_LOOP, 1) + + # Join group + if addrinfo[0] == socket.AF_INET: # IPv4 + self._socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP) + + # Allow multiple copies of this program on one machine + # (not strictly needed) + self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + self._socket.bind((defines.ALL_COAP_NODES, self.server_address[1])) + mreq = struct.pack("4sl", socket.inet_aton(defines.ALL_COAP_NODES), socket.INADDR_ANY) + self._socket.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq) + self._unicast_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + self._unicast_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + self._unicast_socket.bind(self.server_address) + else: + self._socket = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM, socket.IPPROTO_UDP) + + # Allow multiple copies of this program on one machine + # (not strictly needed) + self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + self._socket.bind((defines.ALL_COAP_NODES_IPV6, self.server_address[1])) + + addrinfo_multicast = socket.getaddrinfo(defines.ALL_COAP_NODES_IPV6, 5683)[0] + group_bin = socket.inet_pton(socket.AF_INET6, addrinfo_multicast[4][0]) + mreq = group_bin + struct.pack('@I', 0) + self._socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_JOIN_GROUP, mreq) + self._unicast_socket = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM) + self._unicast_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + self._unicast_socket.bind(self.server_address) + + else: + if addrinfo[0] == socket.AF_INET: # IPv4 + self._socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + else: + self._socket = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM) + self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + + self._socket.bind(self.server_address) + + def purge(self): + """ + Clean old transactions + + """ + while not self.stopped.isSet(): + self.stopped.wait(timeout=defines.EXCHANGE_LIFETIME) + self._messageLayer.purge() + + 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) + if len(client_address) > 2: + client_address = (client_address[0], client_address[1]) + except socket.timeout: + continue + except Exception as e: + if self._cb_ignore_listen_exception is not None and callable(self._cb_ignore_listen_exception): + if self._cb_ignore_listen_exception(e, self): + continue + raise + try: + serializer = Serializer() + message = serializer.deserialize(data, client_address) + if isinstance(message, int): + logger.error("receive_datagram - BAD REQUEST") + + rst = Message() + rst.destination = client_address + rst.type = defines.Types["RST"] + rst.code = message + rst.mid = self._messageLayer.fetch_mid() + self.send_datagram(rst) + continue + + logger.debug("receive_datagram - " + str(message)) + if isinstance(message, Request): + transaction = self._messageLayer.receive_request(message) + if transaction.request.duplicated and transaction.completed: + logger.debug("message duplicated, transaction completed") + if transaction.response is not None: + self.send_datagram(transaction.response) + continue + elif transaction.request.duplicated and not transaction.completed: + logger.debug("message duplicated, transaction NOT completed") + self._send_ack(transaction) + continue + args = (transaction, ) + t = threading.Thread(target=self.receive_request, args=args) + t.start() + # self.receive_datagram(data, client_address) + elif isinstance(message, Response): + logger.error("Received response from %s", message.source) + + else: # is Message + transaction = self._messageLayer.receive_empty(message) + if transaction is not None: + with transaction: + self._blockLayer.receive_empty(message, transaction) + self._observeLayer.receive_empty(message, transaction) + + except RuntimeError: + logger.exception("Exception with Executor") + self._socket.close() + + def close(self): + """ + Stop the server. + + """ + logger.info("Stop server") + self.stopped.set() + for event in self.to_be_stopped: + event.set() + + def receive_request(self, transaction): + """ + Handle requests coming from the udp socket. + + :param transaction: the transaction created to manage the request + """ + + with transaction: + + transaction.separate_timer = self._start_separate_timer(transaction) + + self._blockLayer.receive_request(transaction) + + if transaction.block_transfer: + self._stop_separate_timer(transaction.separate_timer) + self._messageLayer.send_response(transaction) + self.send_datagram(transaction.response) + return + + self._observeLayer.receive_request(transaction) + + self._requestLayer.receive_request(transaction) + + if transaction.resource is not None and transaction.resource.changed: + self.notify(transaction.resource) + transaction.resource.changed = False + elif transaction.resource is not None and transaction.resource.deleted: + self.notify(transaction.resource) + transaction.resource.deleted = False + + self._observeLayer.send_response(transaction) + + self._blockLayer.send_response(transaction) + + self._stop_separate_timer(transaction.separate_timer) + + self._messageLayer.send_response(transaction) + + if transaction.response is not None: + if transaction.response.type == defines.Types["CON"]: + self._start_retransmission(transaction, transaction.response) + self.send_datagram(transaction.response) + + def send_datagram(self, message): + """ + Send a message through the udp socket. + + :type message: Message + :param message: the message to send + """ + if not self.stopped.isSet(): + host, port = message.destination + logger.debug("send_datagram - " + str(message)) + serializer = Serializer() + message = serializer.serialize(message) + if self.multicast: + self._unicast_socket.sendto(message, (host, port)) + else: + self._socket.sendto(message, (host, port)) + + def add_resource(self, path, resource): + """ + Helper function to add resources to the resource directory during server initialization. + + :param path: the path for the new created resource + :type resource: Resource + :param resource: the resource to be added + """ + + 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 + + def remove_resource(self, path): + """ + Helper function to remove resources. + + :param path: the path for the unwanted resource + :rtype : the removed object + """ + + 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 not None: + del(self.root[actual_path]) + return res + + def _start_retransmission(self, transaction, message): + """ + Start the retransmission task. + + :type transaction: Transaction + :param transaction: the transaction that owns the message that needs retransmission + :type message: Message + :param message: the message that needs the retransmission task + """ + with transaction: + if message.type == defines.Types['CON']: + future_time = random.uniform(defines.ACK_TIMEOUT, (defines.ACK_TIMEOUT * defines.ACK_RANDOM_FACTOR)) + transaction.retransmit_thread = threading.Thread(target=self._retransmit, + args=(transaction, message, future_time, 0)) + transaction.retransmit_stop = threading.Event() + self.to_be_stopped.append(transaction.retransmit_stop) + transaction.retransmit_thread.start() + + def _retransmit(self, transaction, message, future_time, retransmit_count): + """ + Thread function to retransmit the message in the future + + :param transaction: the transaction that owns the message that needs retransmission + :param message: the message that needs the retransmission task + :param future_time: the amount of time to wait before a new attempt + :param retransmit_count: the number of retransmissions + """ + with transaction: + while retransmit_count < defines.MAX_RETRANSMIT and (not message.acknowledged and not message.rejected) \ + and not self.stopped.isSet(): + if transaction.retransmit_stop is not None: + transaction.retransmit_stop.wait(timeout=future_time) + if not message.acknowledged and not message.rejected and not self.stopped.isSet(): + retransmit_count += 1 + future_time *= 2 + self.send_datagram(message) + + if message.acknowledged or message.rejected: + message.timeouted = False + else: + logger.warning("Give up on message {message}".format(message=message.line_print)) + message.timeouted = True + if message.observe is not None: + self._observeLayer.remove_subscriber(message) + + try: + self.to_be_stopped.remove(transaction.retransmit_stop) + except ValueError: + pass + transaction.retransmit_stop = None + transaction.retransmit_thread = None + + def _start_separate_timer(self, transaction): + """ + Start a thread to handle separate mode. + + :type transaction: Transaction + :param transaction: the transaction that is in processing + :rtype : the Timer object + """ + t = threading.Timer(defines.ACK_TIMEOUT, self._send_ack, (transaction,)) + t.start() + return t + + @staticmethod + def _stop_separate_timer(timer): + """ + Stop the separate Thread if an answer has been already provided to the client. + + :param timer: The Timer object + """ + timer.cancel() + + def _send_ack(self, transaction): + """ + Sends an ACK message for the request. + + :param transaction: the transaction that owns the request + """ + + ack = Message() + ack.type = defines.Types['ACK'] + # TODO handle mutex on transaction + if not transaction.request.acknowledged and transaction.request.type == defines.Types["CON"]: + ack = self._messageLayer.send_empty(transaction, transaction.request, ack) + self.send_datagram(ack) + + def notify(self, resource): + """ + Notifies the observers of a certain resource. + + :param resource: the resource + """ + observers = self._observeLayer.notify(resource) + logger.debug("Notify") + for transaction in observers: + with transaction: + transaction.response = None + transaction = self._requestLayer.receive_request(transaction) + transaction = self._observeLayer.send_response(transaction) + transaction = self._blockLayer.send_response(transaction) + transaction = self._messageLayer.send_response(transaction) + if transaction.response is not None: + if transaction.response.type == defines.Types["CON"]: + self._start_retransmission(transaction, transaction.response) + + self.send_datagram(transaction.response) diff --git a/coapthon/transaction.py b/coapthon/transaction.py new file mode 100644 index 0000000..234b63a --- /dev/null +++ b/coapthon/transaction.py @@ -0,0 +1,156 @@ +import threading + +__author__ = 'Giacomo Tanganelli' + + +class Transaction(object): + """ + Transaction object to bind together a request, a response and a resource. + """ + def __init__(self, request=None, response=None, resource=None, timestamp=None): + """ + Initialize a Transaction object. + + :param request: the request + :param response: the response + :param resource: the resource interested by the transaction + :param timestamp: the timestamp of the transaction + """ + self._response = response + self._request = request + self._resource = resource + self._timestamp = timestamp + self._completed = False + self._block_transfer = False + self.notification = False + self.separate_timer = None + self.retransmit_thread = None + self.retransmit_stop = None + self._lock = threading.RLock() + + self.cacheHit = False + self.cached_element = None + + def __enter__(self): + self._lock.acquire() + + def __exit__(self, exc_type, exc_val, exc_tb): + self._lock.release() + + @property + def response(self): + """ + Return the response. + + :return: the response + :rtype: Response + """ + return self._response + + @response.setter + def response(self, value): + """ + Set the response. + + :type value: Response + :param value: the response to be set in the transaction + """ + self._response = value + + @property + def request(self): + """ + Return the request. + + :return: the request + :rtype: Request + """ + return self._request + + @request.setter + def request(self, value): + """ + Set the request. + + :type value: Request + :param value: the request to be set in the transaction + """ + self._request = value + + @property + def resource(self): + """ + Return the resource. + + :return: the resource + :rtype: Resource + """ + return self._resource + + @resource.setter + def resource(self, value): + """ + Set the resource. + + :type value: Resource + :param value: the resource to be set in the transaction + """ + self._resource = value + + @property + def timestamp(self): + """ + Return the timestamp. + + :return: the timestamp + """ + return self._timestamp + + @timestamp.setter + def timestamp(self, t): + """ + Set the timestamp. + + :param t: the timestamp of the transaction + """ + self._timestamp = t + + @property + def completed(self): + """ + Return the completed attribute. + + :return: True, if transaction is completed + """ + return self._completed + + @completed.setter + def completed(self, b): + """ + Set the completed attribute. + + :param b: the completed value + :type b: bool + """ + assert isinstance(b, bool) + self._completed = b + + @property + def block_transfer(self): + """ + Return the block_transfer attribute. + + :return: True, if transaction is blockwise + """ + return self._block_transfer + + @block_transfer.setter + def block_transfer(self, b): + """ + Set the block_transfer attribute. + + :param b: the block_transfer value + :type b: bool + """ + assert isinstance(b, bool) + self._block_transfer = b diff --git a/coapthon/utils.py b/coapthon/utils.py new file mode 100644 index 0000000..505054c --- /dev/null +++ b/coapthon/utils.py @@ -0,0 +1,188 @@ +import random +import string + +__author__ = 'Giacomo Tanganelli' + + +def check_nocachekey(option): + """ + checks if an option is a NoCacheKey option or Etag + + :param option: + :return: + """ + return ((option.number & 0x1E) == 0x1C) | (option.number == 4) + + +def check_code(code): + """ + checks if the response code is one of the valid ones defined in the rfc + + :param code: + :return: + """ + if (65 <= code <= 69) or (128 <= code <= 134) or (code == 140) or (code == 141) or (code == 143) or ( + 160 <= code <= 165): + return + + else: + raise InvalidResponseCode + +""" +exception used to signal an invalid response code +""" + + +class InvalidResponseCode: + def __init__(self, code): + self.inv_code = code + + +def is_uri_option(number): + """ + checks if the option is part of uri-path, uri-host, uri-port, uri-query + + :param number: + :return: + """ + if number == 3 | number == 7 | number == 11 | number == 15: + return True + return False + + +def generate_random_token(size): + return ''.join(random.choice(string.ascii_letters) for _ in range(size)) + + +def parse_blockwise(value): + """ + Parse Blockwise option. + + :param value: option value + :return: num, m, size + """ + + length = byte_len(value) + if length == 1: + num = value & 0xF0 + num >>= 4 + m = value & 0x08 + m >>= 3 + size = value & 0x07 + elif length == 2: + num = value & 0xFFF0 + num >>= 4 + m = value & 0x0008 + m >>= 3 + size = value & 0x0007 + else: + num = value & 0xFFFFF0 + num >>= 4 + m = value & 0x000008 + m >>= 3 + size = value & 0x000007 + return num, int(m), pow(2, (size + 4)) + + +def byte_len(int_type): + """ + Get the number of byte needed to encode the int passed. + + :param int_type: the int to be converted + :return: the number of bits needed to encode the int passed. + """ + length = 0 + while int_type: + int_type >>= 1 + length += 1 + if length > 0: + if length % 8 != 0: + length = int(length / 8) + 1 + else: + length = int(length / 8) + return length + + +def parse_uri(uri): + t = uri.split("://") + tmp = t[1] + t = tmp.split("/", 1) + tmp = t[0] + path = t[1] + if tmp.startswith("["): + t = tmp.split("]") + host = t[0][1:] + port = int(t[1][1:]) + else: + t = tmp.split(":", 1) + try: + host = t[0] + port = int(t[1]) + except IndexError: + host = tmp + port = 5683 + + return str(host), port, path + + +def create_logging(): # pragma: no cover + with open("logging.conf", "w") as f: + f.writelines("[loggers]\n") + f.writelines("keys=root\n\n") + f.writelines("[handlers]\n") + f.writelines("keys=consoleHandler\n\n") + f.writelines("[formatters]\n") + f.writelines("keys=simpleFormatter\n\n") + f.writelines("[logger_root]\n") + f.writelines("level=DEBUG\n") + f.writelines("handlers=consoleHandler\n\n") + f.writelines("[handler_consoleHandler]\n") + f.writelines("class=StreamHandler\n") + f.writelines("level=DEBUG\n") + f.writelines("formatter=simpleFormatter\n") + f.writelines("args=(sys.stdout,)\n\n") + f.writelines("[formatter_simpleFormatter]\n") + f.writelines("format=%(asctime)s - %(threadName)-10s - %(name)s - %(levelname)s - %(message)s\n") + f.writelines("datefmt=") + + +class Tree(object): + def __init__(self): + self.tree = {} + + def dump(self): + """ + Get all the paths registered in the server. + + :return: registered resources. + """ + return sorted(list(self.tree.keys())) + + def with_prefix(self, path): + ret = [] + for key in list(self.tree.keys()): + if path.startswith(key): + ret.append(key) + + if len(ret) > 0: + return ret + raise KeyError + + def with_prefix_resource(self, path): + ret = [] + for key, value in self.tree.items(): + if path.startswith(key): + ret.append(value) + + if len(ret) > 0: + return ret + raise KeyError + + def __getitem__(self, item): + return self.tree[item] + + def __setitem__(self, key, value): + self.tree[key] = value + + def __delitem__(self, key): + del self.tree[key] diff --git a/coapthon/utils.py.bak b/coapthon/utils.py.bak new file mode 100644 index 0000000..d107033 --- /dev/null +++ b/coapthon/utils.py.bak @@ -0,0 +1,188 @@ +import random +import string + +__author__ = 'Giacomo Tanganelli' + + +def check_nocachekey(option): + """ + checks if an option is a NoCacheKey option or Etag + + :param option: + :return: + """ + return ((option.number & 0x1E) == 0x1C) | (option.number == 4) + + +def check_code(code): + """ + checks if the response code is one of the valid ones defined in the rfc + + :param code: + :return: + """ + if (65 <= code <= 69) or (128 <= code <= 134) or (code == 140) or (code == 141) or (code == 143) or ( + 160 <= code <= 165): + return + + else: + raise InvalidResponseCode + +""" +exception used to signal an invalid response code +""" + + +class InvalidResponseCode: + def __init__(self, code): + self.inv_code = code + + +def is_uri_option(number): + """ + checks if the option is part of uri-path, uri-host, uri-port, uri-query + + :param number: + :return: + """ + if number == 3 | number == 7 | number == 11 | number == 15: + return True + return False + + +def generate_random_token(size): + return ''.join(random.choice(string.ascii_letters) for _ in range(size)) + + +def parse_blockwise(value): + """ + Parse Blockwise option. + + :param value: option value + :return: num, m, size + """ + + length = byte_len(value) + if length == 1: + num = value & 0xF0 + num >>= 4 + m = value & 0x08 + m >>= 3 + size = value & 0x07 + elif length == 2: + num = value & 0xFFF0 + num >>= 4 + m = value & 0x0008 + m >>= 3 + size = value & 0x0007 + else: + num = value & 0xFFFFF0 + num >>= 4 + m = value & 0x000008 + m >>= 3 + size = value & 0x000007 + return num, int(m), pow(2, (size + 4)) + + +def byte_len(int_type): + """ + Get the number of byte needed to encode the int passed. + + :param int_type: the int to be converted + :return: the number of bits needed to encode the int passed. + """ + length = 0 + while int_type: + int_type >>= 1 + length += 1 + if length > 0: + if length % 8 != 0: + length = int(length / 8) + 1 + else: + length = int(length / 8) + return length + + +def parse_uri(uri): + t = uri.split("://") + tmp = t[1] + t = tmp.split("/", 1) + tmp = t[0] + path = t[1] + if tmp.startswith("["): + t = tmp.split("]") + host = t[0][1:] + port = int(t[1][1:]) + else: + t = tmp.split(":", 1) + try: + host = t[0] + port = int(t[1]) + except IndexError: + host = tmp + port = 5683 + + return str(host), port, path + + +def create_logging(): # pragma: no cover + with open("logging.conf", "w") as f: + f.writelines("[loggers]\n") + f.writelines("keys=root\n\n") + f.writelines("[handlers]\n") + f.writelines("keys=consoleHandler\n\n") + f.writelines("[formatters]\n") + f.writelines("keys=simpleFormatter\n\n") + f.writelines("[logger_root]\n") + f.writelines("level=DEBUG\n") + f.writelines("handlers=consoleHandler\n\n") + f.writelines("[handler_consoleHandler]\n") + f.writelines("class=StreamHandler\n") + f.writelines("level=DEBUG\n") + f.writelines("formatter=simpleFormatter\n") + f.writelines("args=(sys.stdout,)\n\n") + f.writelines("[formatter_simpleFormatter]\n") + f.writelines("format=%(asctime)s - %(threadName)-10s - %(name)s - %(levelname)s - %(message)s\n") + f.writelines("datefmt=") + + +class Tree(object): + def __init__(self): + self.tree = {} + + def dump(self): + """ + Get all the paths registered in the server. + + :return: registered resources. + """ + return self.tree.keys() + + def with_prefix(self, path): + ret = [] + for key in self.tree.keys(): + if path.startswith(key): + ret.append(key) + + if len(ret) > 0: + return ret + raise KeyError + + def with_prefix_resource(self, path): + ret = [] + for key, value in self.tree.iteritems(): + if path.startswith(key): + ret.append(value) + + if len(ret) > 0: + return ret + raise KeyError + + def __getitem__(self, item): + return self.tree[item] + + def __setitem__(self, key, value): + self.tree[key] = value + + def __delitem__(self, key): + del self.tree[key] diff --git a/collectclient.py b/collectclient.py new file mode 100644 index 0000000..d3f39c6 --- /dev/null +++ b/collectclient.py @@ -0,0 +1,141 @@ +#!/usr/bin/env python +import getopt +import json +import socket +import sys + +from coapthon.client.helperclient import HelperClient +from coapthon.utils import parse_uri + +__author__ = 'Giacomo Tanganelli' + +client = None + + +def usage(): # pragma: no cover + print("Command:\tcollectclient.py -c ") + print("Options:") + print("\t-c, --config=\t\tConfig file") + + +def client_callback(response): + print("Callback") + + +def client_callback_observe(response): # pragma: no cover + global client + print("Callback_observe") + print((response.pretty_print())) + check = True + while check: + chosen = eval(input("Stop observing? [y/N]: ")) + if chosen != "" and not (chosen == "n" or chosen == "N" or chosen == "y" or chosen == "Y"): + print("Unrecognized choose.") + continue + elif chosen == "y" or chosen == "Y": + while True: + rst = eval(input("Send RST message? [Y/n]: ")) + if rst != "" and not (rst == "n" or rst == "N" or rst == "y" or rst == "Y"): + print("Unrecognized choose.") + continue + elif rst == "" or rst == "y" or rst == "Y": + client.cancel_observing(response, True) + else: + client.cancel_observing(response, False) + check = False + break + else: + break + + +def main(): # pragma: no cover + global client + config = None + try: + opts, args = getopt.getopt(sys.argv[1:], "hc:", ["help", "config="]) + except getopt.GetoptError as err: + # print help information and exit: + print((str(err))) # will print something like "option -a not recognized" + usage() + sys.exit(2) + for o, a in opts: + if o in ("-c", "--config"): + config = a + elif o in ("-h", "--help"): + usage() + sys.exit() + else: + usage() + sys.exit(2) + + if config is None: + print("Config file must be specified") + usage() + sys.exit(2) + + config = open(config, "r") + config = json.load(config) + for n in config["nodes"]: + path = "coap://"+n["ip"]+":"+str(n["port"])+"/radio" + host, port, path = parse_uri(path) + try: + tmp = socket.gethostbyname(host) + host = tmp + except socket.gaierror: + pass + client = HelperClient(server=(host, port)) + response = client.get(path) + print((response.pretty_print())) + client.stop() + + # if op == "OBSERVE": + # if path is None: + # print "Path cannot be empty for a GET request" + # usage() + # sys.exit(2) + # client.observe(path, client_callback_observe) + # + # elif op == "DELETE": + # if path is None: + # print "Path cannot be empty for a DELETE request" + # usage() + # sys.exit(2) + # response = client.delete(path) + # print response.pretty_print() + # client.stop() + # elif op == "POST": + # if path is None: + # print "Path cannot be empty for a POST request" + # usage() + # sys.exit(2) + # if payload is None: + # print "Payload cannot be empty for a POST request" + # usage() + # sys.exit(2) + # response = client.post(path, payload) + # print response.pretty_print() + # client.stop() + # elif op == "PUT": + # if path is None: + # print "Path cannot be empty for a PUT request" + # usage() + # sys.exit(2) + # if payload is None: + # print "Payload cannot be empty for a PUT request" + # usage() + # sys.exit(2) + # response = client.put(path, payload) + # print response.pretty_print() + # client.stop() + # elif op == "DISCOVER": + # response = client.discover() + # print response.pretty_print() + # client.stop() + # else: + # print "Operation not recognized" + # usage() + # sys.exit(2) + + +if __name__ == '__main__': # pragma: no cover + main() diff --git a/collectserver.py b/collectserver.py new file mode 100644 index 0000000..a71ac1b --- /dev/null +++ b/collectserver.py @@ -0,0 +1,337 @@ +#!/usr/bin/env python + +import getopt +import json +import random +import sys +import threading + +import time + +from coapthon import defines +from coapthon.resources.resource import Resource +from coapthon.server.coap import CoAP + + +__author__ = 'Giacomo Tanganelli' + + +class PowerResource(Resource): + def __init__(self, name="PowerResource", coap_server=None): + super(PowerResource, self).__init__(name, coap_server, visible=True, + observable=True, allow_children=False) + self.resource_type = "Power Resource" + self.content_type = "application/json" + self.cpu_power = 0 + self.lpm_power = 0 + self.listen_power = 0 + self.transmit_power = 0 + self.average_power = 0 + self.aggregate_power = 0 + self.period = 5 + self.read_sensor(True) + + self.value = [{"n": "cpu", "v": self.cpu_power, "u": "mW", "bt": time.time()}, + {"n": "lpm", "v": self.lpm_power, "u": "mW"}, + {"n": "listen", "v": self.listen_power, "u": "mW"}, + {"n": "transmit", "v": self.transmit_power, "u": "mW"}, + {"n": "average", "v": self.average_power, "u": "mW"}, + {"n": "aggregate", "v": self.aggregate_power, "u": "mW"}] + + def render_GET(self, request): + self.value = [{"n": "cpu", "v": self.cpu_power, "u": "mW", "bt": time.time()}, + {"n": "lpm", "v": self.lpm_power, "u": "mW"}, + {"n": "listen", "v": self.listen_power, "u": "mW"}, + {"n": "transmit", "v": self.transmit_power, "u": "mW"}, + {"n": "average", "v": self.average_power, "u": "mW"}, + {"n": "aggregate", "v": self.aggregate_power, "u": "mW"}] + + self.payload = (defines.Content_types["application/json"], json.dumps(self.value)) + return self + + def read_sensor(self, first=False): + self.cpu_power = random.uniform(0, 0.3) + self.lpm_power = random.uniform(0, 0.15) + self.listen_power = random.uniform(0, 0.4) + self.transmit_power = random.uniform(0, 0.2) + self.average_power = 0 + self.aggregate_power = self.cpu_power + self.lpm_power + self.listen_power + self.transmit_power + + self.value = [{"n": "cpu", "v": self.cpu_power, "u": "mW", "bt": time.time()}, + {"n": "lpm", "v": self.lpm_power, "u": "mW"}, + {"n": "listen", "v": self.listen_power, "u": "mW"}, + {"n": "transmit", "v": self.transmit_power, "u": "mW"}, + {"n": "average", "v": self.average_power, "u": "mW"}, + {"n": "aggregate", "v": self.aggregate_power, "u": "mW"}] + + self.payload = (defines.Content_types["application/json"], json.dumps(self.value)) + if not self._coap_server.stopped.isSet(): + + timer = threading.Timer(self.period, self.read_sensor) + timer.setDaemon(True) + timer.start() + + if not first and self._coap_server is not None: + self._coap_server.notify(self) + self.observe_count += 1 + + +class TemperatureResource(Resource): + def __init__(self, name="TemperatureResource", coap_server=None): + super(TemperatureResource, self).__init__(name, coap_server, visible=True, + observable=True, allow_children=False) + self.resource_type = "Temperature Resource" + self.content_type = "application/json" + self.temperature = 0 + self.period = 5 + self.read_sensor(True) + + self.value = [{"n": "temperature", "v": self.temperature, "u": "Cel", "t": time.time()}] + + def render_GET(self, request): + self.value = [{"n": "temperature", "v": self.temperature, "u": "Cel", "t": time.time()}] + + self.payload = (defines.Content_types["application/json"], json.dumps(self.value)) + return self + + def read_sensor(self, first=False): + self.temperature = random.uniform(-10, 30) + + self.value = [{"n": "temperature", "v": self.temperature, "u": "Cel", "t": time.time()}] + + self.payload = (defines.Content_types["application/json"], json.dumps(self.value)) + if not self._coap_server.stopped.isSet(): + + timer = threading.Timer(self.period, self.read_sensor) + timer.setDaemon(True) + timer.start() + + if not first and self._coap_server is not None: + self._coap_server.notify(self) + self.observe_count += 1 + + +class BatteryResource(Resource): + def __init__(self, name="BatteryResource", coap_server=None): + super(BatteryResource, self).__init__(name, coap_server, visible=True, + observable=True, allow_children=False) + self.resource_type = "Battery Resource" + self.content_type = "application/json" + self.voltage = 0 + self.indicator = 0 + self.period = 5 + self.read_sensor(True) + + self.value = [{"n": "voltage", "v": self.voltage, "u": "V", "bt": time.time()}, + {"n": "indicator", "v": self.indicator, "u": "%"}] + + def render_GET(self, request): + self.value = [{"n": "voltage", "v": self.voltage, "u": "V", "bt": time.time()}, + {"n": "indicator", "v": self.indicator, "u": "%"}] + + self.payload = (defines.Content_types["application/json"], json.dumps(self.value)) + return self + + def read_sensor(self, first=False): + self.voltage = random.uniform(0, 5) + self.indicator = random.randint(1, 10) + + self.value = [{"n": "voltage", "v": self.voltage, "u": "V", "bt": time.time()}, + {"n": "indicator", "v": self.indicator, "u": "%"}] + + self.payload = (defines.Content_types["application/json"], json.dumps(self.value)) + if not self._coap_server.stopped.isSet(): + + timer = threading.Timer(self.period, self.read_sensor) + timer.setDaemon(True) + timer.start() + + if not first and self._coap_server is not None: + self._coap_server.notify(self) + self.observe_count += 1 + + +class RadioResource(Resource): + def __init__(self, name="RadioResource", coap_server=None): + super(RadioResource, self).__init__(name, coap_server, visible=True, + observable=True, allow_children=False) + self.resource_type = "Radio Resource" + self.content_type = "application/json" + self.rssi = 0 + self.latency = 0 + self.best_neighbor_id = "0" + self.best_neighbor_etx = 0 + self.byte_sent = 0 + self.byte_received = 0 + self.period = 5 + self.read_sensor(True) + + self.value = [{"n": "rssi", "v": self.rssi, "u": "dBm", "bt": time.time()}, + {"n": "latency", "v": self.latency, "u": "ms"}, + {"n": "best_neighbor_id", "vs": self.best_neighbor_id}, + {"n": "best_neighbor_etx", "v": self.best_neighbor_etx}, + {"n": "byte_sent", "v": self.byte_sent}, + {"n": "byte_received", "v": self.byte_received}] + + def render_GET(self, request): + self.value = [{"n": "rssi", "v": self.rssi, "u": "dBm", "bt": time.time()}, + {"n": "latency", "v": self.latency, "u": "ms"}, + {"n": "best_neighbor_id", "vs": self.best_neighbor_id}, + {"n": "best_neighbor_etx", "v": self.best_neighbor_etx}, + {"n": "byte_sent", "v": self.byte_sent}, + {"n": "byte_received", "v": self.byte_received}] + + self.payload = (defines.Content_types["application/json"], json.dumps(self.value)) + return self + + def read_sensor(self, first=False): + self.rssi = random.uniform(-90, -10) + self.latency = random.uniform(0, 50) + self.best_neighbor_id = "0" + self.best_neighbor_etx = random.randint(1, 10) + self.byte_sent = random.randint(1, 500) + self.byte_received = random.randint(1, 500) + + self.value = [{"n": "rssi", "v": self.rssi, "u": "dBm", "bt": time.time()}, + {"n": "latency", "v": self.latency, "u": "ms"}, + {"n": "best_neighbor_id", "vs": self.best_neighbor_id}, + {"n": "best_neighbor_etx", "v": self.best_neighbor_etx}, + {"n": "byte_sent", "v": self.byte_sent}, + {"n": "byte_received", "v": self.byte_received}] + self.payload = (defines.Content_types["application/json"], json.dumps(self.value)) + + if not self._coap_server.stopped.isSet(): + + timer = threading.Timer(self.period, self.read_sensor) + timer.setDaemon(True) + timer.start() + + if not first and self._coap_server is not None: + self._coap_server.notify(self) + self.observe_count += 1 + +class HumidityResource(Resource): + def __init__(self, name="HumidityResource", coap_server=None): + super(HumidityResource, self).__init__(name, coap_server, visible=True, + observable=True, allow_children=False) + self.resource_type = "Humidity Resource" + self.content_type = "application/json" + self.humidity = 0 + self.period = 5 + self.read_sensor(True) + + self.value = [{"n": "humidity", "v": self.humidity, "u": "%RH", "t": time.time()}] + + def render_GET(self, request): + self.value = [{"n": "humidity", "v": self.humidity, "u": "%RH", "t": time.time()}] + + self.payload = (defines.Content_types["application/json"], json.dumps(self.value)) + return self + + def read_sensor(self, first=False): + self.humidity = random.randint(0, 100) + + self.value = [{"n": "humidity", "v": self.humidity, "u": "%RH", "t": time.time()}] + self.payload = (defines.Content_types["application/json"], json.dumps(self.value)) + if not self._coap_server.stopped.isSet(): + + timer = threading.Timer(self.period, self.read_sensor) + timer.setDaemon(True) + timer.start() + + if not first and self._coap_server is not None: + self._coap_server.notify(self) + self.observe_count += 1 + + +class LightResource(Resource): + def __init__(self, name="LightResource", coap_server=None): + super(LightResource, self).__init__(name, coap_server, visible=True, + observable=True, allow_children=False) + self.resource_type = "Light Resource" + self.content_type = "application/json" + self.light1 = 0 + self.light2 = 0 + + self.value = [{"n": "light1", "v": self.light1, "u": "lx", "bt": time.time()}, + {"n": "light2", "v": self.light2, "u": "lx"}] + self.period = 5 + self.read_sensor(True) + + def render_GET(self, request): + self.value = [{"n": "light1", "v": self.light1, "u": "lx", "bt": time.time()}, + {"n": "light2", "v": self.light2, "u": "lx"}] + + self.payload = (defines.Content_types["application/json"], json.dumps(self.value)) + return self + + def read_sensor(self, first=False): + self.light1 = random.randint(0, 1000) + self.light2 = random.randint(0, 2000) + self.value = [{"n": "light1", "v": self.light1, "u": "lx", "bt": time.time()}, + {"n": "light2", "v": self.light2, "u": "lx"}] + self.payload = (defines.Content_types["application/json"], json.dumps(self.value)) + if not self._coap_server.stopped.isSet(): + + timer = threading.Timer(self.period, self.read_sensor) + timer.setDaemon(True) + timer.start() + + if not first and self._coap_server is not None: + self._coap_server.notify(self) + self.observe_count += 1 + + +class CoAPServer(CoAP): + def __init__(self, host, port, multicast=False): + CoAP.__init__(self, (host, port), multicast) + print(("CoAP Server start on " + host + ":" + str(port))) + + +def usage(): # pragma: no cover + print("coapserver.py -i -p ") + + +def main(argv): # pragma: no cover + ip = "0.0.0.0" + port = 5683 + multicast = False + try: + opts, args = getopt.getopt(argv, "hi:p:m", ["ip=", "port=", "multicast"]) + except getopt.GetoptError: + usage() + sys.exit(2) + for opt, arg in opts: + if opt == '-h': + usage() + sys.exit() + elif opt in ("-i", "--ip"): + ip = arg + elif opt in ("-p", "--port"): + port = int(arg) + elif opt in ("-m", "--multicast"): + multicast = True + + server = CoAPServer(ip, port, multicast) + power = PowerResource(coap_server=server) + temperature = TemperatureResource(coap_server=server) + battery = BatteryResource(coap_server=server) + radio = RadioResource(coap_server=server) + hum = HumidityResource(coap_server=server) + light = LightResource(coap_server=server) + server.add_resource('power/', power) + server.add_resource('temperature/', temperature) + server.add_resource('battery/', battery) + server.add_resource('radio/', radio) + server.add_resource('humidity/', hum) + server.add_resource('light/', light) + try: + server.listen(10) + except KeyboardInterrupt: + print("Server Shutdown") + server.close() + print("Exiting...") + + +if __name__ == "__main__": # pragma: no cover + main(sys.argv[1:]) diff --git a/coverage_test.py b/coverage_test.py new file mode 100644 index 0000000..07a0c64 --- /dev/null +++ b/coverage_test.py @@ -0,0 +1,1576 @@ +from queue import Queue +import random +import socket +import threading +import unittest +from coapclient import HelperClient +from coapserver import CoAPServer +from coapthon import defines +from coapthon.messages.message import Message +from coapthon.messages.option import Option +from coapthon.messages.request import Request +from coapthon.messages.response import Response +from coapthon.serializer import Serializer + +__author__ = 'Giacomo Tanganelli' +__version__ = "2.0" + + +class Tests(unittest.TestCase): + + def setUp(self): + self.server_address = ("127.0.0.1", 5683) + self.current_mid = random.randint(1, 1000) + self.server_mid = random.randint(1000, 2000) + self.server = CoAPServer("127.0.0.1", 5683) + self.server_thread = threading.Thread(target=self.server.listen, args=(10,)) + self.server_thread.start() + self.queue = Queue() + + def tearDown(self): + self.server.close() + self.server_thread.join(timeout=25) + self.server = None + + def _test_with_client(self, message_list): # pragma: no cover + client = HelperClient(self.server_address) + for message, expected in message_list: + if message is not None: + received_message = client.send_request(message) + if expected is not None: + + if expected.type is not None: + self.assertEqual(received_message.type, expected.type) + if expected.mid is not None: + self.assertEqual(received_message.mid, expected.mid) + self.assertEqual(received_message.code, expected.code) + if expected.source is not None: + self.assertEqual(received_message.source, self.server_address) + if expected.token is not None: + self.assertEqual(received_message.token, expected.token) + if expected.payload is not None: + self.assertEqual(received_message.payload, expected.payload) + if expected.options: + self.assertEqual(len(received_message.options), len(expected.options)) + for o in expected.options: + assert isinstance(o, Option) + option_value = getattr(expected, o.name.lower().replace("-", "_")) + option_value_rec = getattr(received_message, o.name.lower().replace("-", "_")) + self.assertEqual(option_value, option_value_rec) + client.stop() + + def _test_with_client_observe(self, message_list): # pragma: no cover + client = HelperClient(self.server_address) + for message, expected in message_list: + if message is not None: + client.send_request(message, self.client_callback) + if expected is not None: + received_message = self.queue.get() + if expected.type is not None: + self.assertEqual(received_message.type, expected.type) + if expected.mid is not None: + self.assertEqual(received_message.mid, expected.mid) + self.assertEqual(received_message.code, expected.code) + if expected.source is not None: + self.assertEqual(received_message.source, self.server_address) + if expected.token is not None: + self.assertEqual(received_message.token, expected.token) + if expected.payload is not None: + self.assertEqual(received_message.payload, expected.payload) + if expected.options: + self.assertEqual(len(received_message.options), len(expected.options)) + for o in expected.options: + assert isinstance(o, Option) + option_value = getattr(expected, o.name.lower().replace("-", "_")) + option_value_rec = getattr(received_message, o.name.lower().replace("-", "_")) + self.assertEqual(option_value, option_value_rec) + client.stop() + + def client_callback(self, response): + print("Callback") + self.queue.put(response) + + def _test_plugtest(self, message_list): # pragma: no cover + serializer = Serializer() + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + for message, expected in message_list: + if message is not None: + datagram = serializer.serialize(message) + sock.sendto(datagram, message.destination) + if expected is not None: + datagram, source = sock.recvfrom(4096) + received_message = serializer.deserialize(datagram, source) + if expected.type is not None: + self.assertEqual(received_message.type, expected.type) + if expected.mid is not None: + self.assertEqual(received_message.mid, expected.mid) + self.assertEqual(received_message.code, expected.code) + if expected.source is not None: + self.assertEqual(received_message.source, source) + if expected.token is not None: + self.assertEqual(received_message.token, expected.token) + if expected.payload is not None: + self.assertEqual(received_message.payload, expected.payload) + if expected.options is not None: + self.assertEqual(received_message.options, expected.options) + for o in expected.options: + assert isinstance(o, Option) + option_value = getattr(expected, o.name.lower().replace("-", "_")) + option_value_rec = getattr(received_message, o.name.lower().replace("-", "_")) + self.assertEqual(option_value, option_value_rec) + sock.close() + + def _test_datagram(self, message_list): # pragma: no cover + serializer = Serializer() + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + for message, expected in message_list: + if message is not None: + datagram, destination = message + sock.sendto(datagram, destination) + if expected is not None: + datagram, source = sock.recvfrom(4096) + received_message = serializer.deserialize(datagram, source) + if expected.type is not None: + self.assertEqual(received_message.type, expected.type) + if expected.mid is not None: + self.assertEqual(received_message.mid, expected.mid) + self.assertEqual(received_message.code, expected.code) + if expected.source is not None: + self.assertEqual(received_message.source, source) + if expected.token is not None: + self.assertEqual(received_message.token, expected.token) + if expected.payload is not None: + self.assertEqual(received_message.payload, expected.payload) + if expected.options is not None: + self.assertEqual(received_message.options, expected.options) + for o in expected.options: + assert isinstance(o, Option) + option_value = getattr(expected, o.name.lower().replace("-", "_")) + option_value_rec = getattr(received_message, o.name.lower().replace("-", "_")) + self.assertEqual(option_value, option_value_rec) + sock.close() + + def test_not_allowed(self): + print("TEST_NOT_ALLOWED") + path = "/void" + req = Request() + req.code = defines.Codes.GET.number + req.uri_path = path + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = self.current_mid + expected.code = defines.Codes.METHOD_NOT_ALLOWED.number + expected.token = None + + exchange1 = (req, expected) + + self.current_mid += 1 + + req = Request() + req.code = defines.Codes.POST.number + req.uri_path = path + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = self.current_mid + expected.code = defines.Codes.METHOD_NOT_ALLOWED.number + expected.token = None + + exchange2 = (req, expected) + + self.current_mid += 1 + + req = Request() + req.code = defines.Codes.PUT.number + req.uri_path = path + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = self.current_mid + expected.code = defines.Codes.METHOD_NOT_ALLOWED.number + expected.token = None + + exchange3 = (req, expected) + + self.current_mid += 1 + + req = Request() + req.code = defines.Codes.DELETE.number + req.uri_path = path + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = self.current_mid + expected.code = defines.Codes.METHOD_NOT_ALLOWED.number + expected.token = None + + exchange4 = (req, expected) + + self.current_mid += 1 + self._test_with_client([exchange1, exchange2, exchange3, exchange4]) + + # def test_separate(self): + # print "TEST_SEPARATE" + # path = "/separate" + # req = Request() + # req.code = defines.Codes.GET.number + # req.uri_path = path + # req.type = defines.Types["CON"] + # req._mid = self.current_mid + # req.destination = self.server_address + # + # expected = Response() + # expected.type = defines.Types["CON"] + # expected._mid = None + # expected.code = defines.Codes.CONTENT.number + # expected.token = None + # expected.max_age = 60 + # + # exchange1 = (req, expected) + # + # self.current_mid += 1 + # + # req = Request() + # req.code = defines.Codes.POST.number + # req.uri_path = path + # req.type = defines.Types["CON"] + # req._mid = self.current_mid + # req.destination = self.server_address + # req.payload = "POST" + # + # expected = Response() + # expected.type = defines.Types["CON"] + # expected._mid = None + # expected.code = defines.Codes.CHANGED.number + # expected.token = None + # expected.options = None + # + # exchange2 = (req, expected) + # + # self.current_mid += 1 + # + # req = Request() + # req.code = defines.Codes.PUT.number + # req.uri_path = path + # req.type = defines.Types["CON"] + # req._mid = self.current_mid + # req.destination = self.server_address + # req.payload = "PUT" + # + # expected = Response() + # expected.type = defines.Types["CON"] + # expected._mid = None + # expected.code = defines.Codes.CHANGED.number + # expected.token = None + # expected.options = None + # + # exchange3 = (req, expected) + # + # self.current_mid += 1 + # + # req = Request() + # req.code = defines.Codes.DELETE.number + # req.uri_path = path + # req.type = defines.Types["CON"] + # req._mid = self.current_mid + # req.destination = self.server_address + # + # expected = Response() + # expected.type = defines.Types["CON"] + # expected._mid = None + # expected.code = defines.Codes.DELETED.number + # expected.token = None + # + # exchange4 = (req, expected) + # + # self.current_mid += 1 + # self._test_with_client([exchange1, exchange2, exchange3, exchange4]) + + def test_post(self): + print("TEST_POST") + path = "/storage/new_res?id=1" + req = Request() + + req.code = defines.Codes.POST.number + req.uri_path = path + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + req.payload = "test" + req.add_if_none_match() + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = self.current_mid + expected.code = defines.Codes.CREATED.number + expected.token = None + expected.payload = None + expected.location_path = "storage/new_res" + expected.location_query = "id=1" + + exchange1 = (req, expected) + self.current_mid += 1 + + req = Request() + req.code = defines.Codes.GET.number + req.uri_path = "/storage/new_res" + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + req.if_match = ["test", "not"] + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = self.current_mid + expected.code = defines.Codes.CONTENT.number + expected.token = None + expected.payload = "test" + + exchange2 = (req, expected) + self.current_mid += 1 + + req = Request() + req.code = defines.Codes.PUT.number + req.uri_path = "/storage/new_res" + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + req.if_match = ["not"] + req.payload = "not" + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = self.current_mid + expected.code = defines.Codes.PRECONDITION_FAILED.number + expected.token = None + + exchange3 = (req, expected) + self.current_mid += 1 + + req = Request() + req.code = defines.Codes.POST.number + req.uri_path = "/storage/new_res" + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + req.if_match = ["not"] + req.payload = "not" + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = self.current_mid + expected.code = defines.Codes.PRECONDITION_FAILED.number + expected.token = None + + exchange4 = (req, expected) + self.current_mid += 1 + + req = Request() + req.code = defines.Codes.PUT.number + req.uri_path = "/storage/new_res" + req._mid = self.current_mid + req.destination = self.server_address + req.add_if_none_match() + req.payload = "not" + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = self.current_mid + expected.code = defines.Codes.PRECONDITION_FAILED.number + expected.token = None + + exchange5 = (req, expected) + self.current_mid += 1 + + self._test_with_client([exchange1, exchange2, exchange3, exchange4, exchange5]) + + def test_post_block(self): + print("TEST_POST_BLOCK") + path = "/storage/new_res" + req = Request() + + req.code = defines.Codes.POST.number + req.uri_path = path + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + req.payload = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras sollicitudin fermentum ornare. " \ + "Cras accumsan tellus quis dui lacinia eleifend. Proin ultrices rutrum orci vitae luctus. " \ + "Nullam malesuada pretium elit, at aliquam odio vehicula in. Etiam nec maximus elit. " \ + "Etiam at erat ac ex ornare feugiat. Curabitur sed malesuada orci, id aliquet nunc. Phasellus " \ + "nec leo luctus, blandit lorem sit amet, interdum metus. Duis efficitur volutpat magna, ac " \ + "ultricies nibh aliquet sit amet. Etiam tempor egestas augue in hendrerit. Nunc eget augue " \ + "ultricies, dignissim lacus et, vulputate dolor. Nulla eros odio, fringilla vel massa ut, " \ + "facilisis cursus quam. Fusce faucibus lobortis congue. Fusce consectetur porta neque, id " \ + "sollicitudin velit maximus eu. Sed pharetra leo quam, vel finibus turpis cursus ac. " \ + "Aenean ac nisi massa. Cras commodo arcu nec ante tristique ullamcorper. Quisque eu hendrerit" \ + " urna. Cras fringilla eros ut nunc maximus, non porta nisl mollis. Aliquam in rutrum massa." \ + " Praesent tristique turpis dui, at ultri" + req.block1 = (1, 1, 1024) + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = None + expected.code = defines.Codes.REQUEST_ENTITY_INCOMPLETE.number + expected.token = None + expected.payload = None + + exchange1 = (req, expected) + self.current_mid += 1 + + req = Request() + req.code = defines.Codes.POST.number + req.uri_path = path + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + req.payload = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras sollicitudin fermentum ornare. " \ + "Cras accumsan tellus quis dui lacinia eleifend. Proin ultrices rutrum orci vitae luctus. " \ + "Nullam malesuada pretium elit, at aliquam odio vehicula in. Etiam nec maximus elit. " \ + "Etiam at erat ac ex ornare feugiat. Curabitur sed malesuada orci, id aliquet nunc. Phasellus " \ + "nec leo luctus, blandit lorem sit amet, interdum metus. Duis efficitur volutpat magna, ac " \ + "ultricies nibh aliquet sit amet. Etiam tempor egestas augue in hendrerit. Nunc eget augue " \ + "ultricies, dignissim lacus et, vulputate dolor. Nulla eros odio, fringilla vel massa ut, " \ + "facilisis cursus quam. Fusce faucibus lobortis congue. Fusce consectetur porta neque, id " \ + "sollicitudin velit maximus eu. Sed pharetra leo quam, vel finibus turpis cursus ac. " \ + "Aenean ac nisi massa. Cras commodo arcu nec ante tristique ullamcorper. Quisque eu hendrerit" \ + " urna. Cras fringilla eros ut nunc maximus, non porta nisl mollis. Aliquam in rutrum massa." \ + " Praesent tristique turpis dui, at ultri" + req.block1 = (0, 1, 1024) + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = None + expected.code = defines.Codes.CONTINUE.number + expected.token = None + expected.payload = None + expected.block1 = (0, 1, 1024) + + exchange2 = (req, expected) + self.current_mid += 1 + + req = Request() + req.code = defines.Codes.POST.number + req.uri_path = path + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + req.payload = "a imperdiet nisl. Quisque a iaculis libero, id tempus lacus. Aenean convallis est non justo " \ + "consectetur, a hendrerit enim consequat. In accumsan ante a egestas luctus. Etiam quis neque " \ + "nec eros vestibulum faucibus. Nunc viverra ipsum lectus, vel scelerisque dui dictum a. Ut orci " \ + "enim, ultrices a ultrices nec, pharetra in quam. Donec accumsan sit amet eros eget fermentum." + req.block1 = (1, 1, 64) + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = None + expected.code = defines.Codes.CONTINUE.number + expected.token = None + expected.payload = None + expected.block1 = (1, 1, 64) + + exchange3 = (req, expected) + self.current_mid += 1 + + req = Request() + req.code = defines.Codes.POST.number + req.uri_path = path + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + req.payload = "a imperdiet nisl. Quisque a iaculis libero, id tempus lacus. Aenean convallis est non justo " \ + "consectetur, a hendrerit enim consequat. In accumsan ante a egestas luctus. Etiam quis neque " \ + "nec eros vestibulum faucibus. Nunc viverra ipsum lectus, vel scelerisque dui dictum a. Ut orci " \ + "enim, ultrices a ultrices nec, pharetra in quam. Donec accumsan sit amet eros eget fermentum." + req.block1 = (3, 1, 64) + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = None + expected.code = defines.Codes.REQUEST_ENTITY_INCOMPLETE.number + expected.token = None + expected.payload = None + + exchange4 = (req, expected) + self.current_mid += 1 + + req = Request() + req.code = defines.Codes.POST.number + req.uri_path = path + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + req.payload = "a imperdiet nisl. Quisque a iaculis libero, id tempus lacus. Aenean convallis est non justo " \ + "consectetur, a hendrerit enim consequat. In accumsan ante a egestas luctus. Etiam quis neque " \ + "nec eros vestibulum faucibus. Nunc viverra ipsum lectus, vel scelerisque dui dictum a. Ut orci " \ + "enim, ultrices a ultrices nec, pharetra in quam. Donec accumsan sit amet eros eget fermentum." + req.block1 = (2, 0, 64) + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = None + expected.code = defines.Codes.CREATED.number + expected.token = None + expected.payload = None + expected.location_path = "storage/new_res" + + exchange5 = (req, expected) + self.current_mid += 1 + + self._test_plugtest([exchange1, exchange2, exchange3, exchange4, exchange5]) + + def test_get_block(self): + print("TEST_GET_BLOCK") + path = "/big" + + req = Request() + req.code = defines.Codes.GET.number + req.uri_path = path + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + req.payload = None + req.block2 = (0, 0, 512) + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = None + expected.code = defines.Codes.CONTENT.number + expected.token = None + expected.payload = None + expected.block2 = (0, 1, 512) + + exchange1 = (req, expected) + self.current_mid += 1 + + req = Request() + req.code = defines.Codes.GET.number + req.uri_path = path + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + req.payload = None + req.block2 = (1, 0, 256) + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = None + expected.code = defines.Codes.CONTENT.number + expected.token = None + expected.payload = None + expected.block2 = (1, 1, 256) + + exchange2 = (req, expected) + self.current_mid += 1 + + req = Request() + req.code = defines.Codes.GET.number + req.uri_path = path + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + req.payload = None + req.block2 = (2, 0, 128) + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = None + expected.code = defines.Codes.CONTENT.number + expected.token = None + expected.payload = None + expected.block2 = (2, 1, 128) + + exchange3 = (req, expected) + self.current_mid += 1 + + req = Request() + req.code = defines.Codes.GET.number + req.uri_path = path + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + req.payload = None + req.block2 = (3, 0, 64) + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = None + expected.code = defines.Codes.CONTENT.number + expected.token = None + expected.payload = None + expected.block2 = (3, 1, 64) + + exchange4 = (req, expected) + self.current_mid += 1 + + req = Request() + req.code = defines.Codes.GET.number + req.uri_path = path + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + req.payload = None + req.block2 = (4, 0, 32) + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = None + expected.code = defines.Codes.CONTENT.number + expected.token = None + expected.payload = None + expected.block2 = (4, 1, 32) + + exchange5 = (req, expected) + self.current_mid += 1 + + req = Request() + req.code = defines.Codes.GET.number + req.uri_path = path + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + req.payload = None + req.block2 = (5, 0, 16) + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = None + expected.code = defines.Codes.CONTENT.number + expected.token = None + expected.payload = None + expected.block2 = (5, 1, 16) + + exchange6 = (req, expected) + self.current_mid += 1 + + req = Request() + req.code = defines.Codes.GET.number + req.uri_path = path + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + req.payload = None + req.block2 = (6, 0, 1024) + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = None + expected.code = defines.Codes.CONTENT.number + expected.token = None + expected.payload = None + expected.block2 = (6, 1, 1024) + + exchange7 = (req, expected) + self.current_mid += 1 + + req = Request() + req.code = defines.Codes.GET.number + req.uri_path = path + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + req.payload = None + req.block2 = (7, 0, 1024) + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = None + expected.code = defines.Codes.CONTENT.number + expected.token = None + expected.payload = None + expected.block2 = (7, 0, 1024) + + exchange8 = (req, expected) + self.current_mid += 1 + + self._test_plugtest([exchange1, exchange2, exchange3, exchange4, exchange5, exchange6, exchange7, exchange8]) + + def test_post_block_big(self): + print("TEST_POST_BLOCK_BIG") + path = "/big" + req = Request() + + req.code = defines.Codes.POST.number + req.uri_path = path + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + req.payload = "Lorem ipsum dolo" + req.block1 = (0, 1, 16) + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = None + expected.code = defines.Codes.CONTINUE.number + expected.token = None + expected.payload = None + expected.block1 = (0, 1, 16) + + exchange1 = (req, expected) + self.current_mid += 1 + + req = Request() + req.code = defines.Codes.POST.number + req.uri_path = path + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + req.payload = "r sit amet, consectetur adipisci" + req.block1 = (1, 1, 32) + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = None + expected.code = defines.Codes.CONTINUE.number + expected.token = None + expected.payload = None + expected.block1 = (1, 1, 32) + + exchange2 = (req, expected) + self.current_mid += 1 + + req = Request() + req.code = defines.Codes.POST.number + req.uri_path = path + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + req.payload = "ng elit. Sed ut ultrices ligula. Pellentesque purus augue, cursu" + req.block1 = (2, 1, 64) + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = None + expected.code = defines.Codes.CONTINUE.number + expected.token = None + expected.payload = None + expected.block1 = (2, 1, 64) + + exchange3 = (req, expected) + self.current_mid += 1 + + req = Request() + req.code = defines.Codes.POST.number + req.uri_path = path + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + req.payload = "s ultricies est in, vehicula congue metus. Vestibulum vel justo lacinia, porttitor quam vitae, " \ + "feugiat sapien. Quisque finibus, " + req.block1 = (3, 1, 128) + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = None + expected.code = defines.Codes.CONTINUE.number + expected.token = None + expected.payload = None + expected.block1 = (3, 1, 128) + + exchange4 = (req, expected) + self.current_mid += 1 + + req = Request() + req.code = defines.Codes.POST.number + req.uri_path = path + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + req.payload = "nisi vitae rhoncus malesuada, augue mauris dapibus tellus, sit amet venenatis libero" \ + " libero sed lorem. In pharetra turpis sed eros porta mollis. Quisque dictum dolor nisl," \ + " imperdiet tincidunt augue malesuada vitae. Donec non felis urna. Suspendisse at hend" + req.block1 = (4, 1, 256) + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = None + expected.code = defines.Codes.CONTINUE.number + expected.token = None + expected.payload = None + expected.block1 = (4, 1, 256) + + exchange5 = (req, expected) + self.current_mid += 1 + + req = Request() + req.code = defines.Codes.POST.number + req.uri_path = path + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + req.payload = "rerit ex, quis aliquet ante. Vivamus ultrices dolor at elit tincidunt, eget fringilla " \ + "ligula vestibulum. In molestie sagittis nibh, ut efficitur tellus faucibus non. Maecenas " \ + "posuere elementum faucibus. Morbi nisi diam, molestie non feugiat et, elementum eget magna." \ + " Donec vel sem facilisis quam viverra ultrices nec eu lacus. Sed molestie nisi id ultrices " \ + "interdum. Curabitur pharetra sed tellus in dignissim. Duis placerat aliquam metus, volutpat " \ + "elementum augue aliquam a. Nunc sed dolor at orci maximus portt" + req.block1 = (5, 1, 512) + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = None + expected.code = defines.Codes.CONTINUE.number + expected.token = None + expected.payload = None + expected.block1 = (5, 1, 512) + + exchange6 = (req, expected) + self.current_mid += 1 + + req = Request() + req.code = defines.Codes.POST.number + req.uri_path = path + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + req.payload = "itor ac sit amet eros. Mauris et nisi in tortor pharetra rhoncus sit amet hendrerit metus. " \ + "Integer laoreet placerat cursus. Nam a nulla ex. Donec laoreet sagittis libero quis " \ + "imperdiet. Vivamus facilisis turpis nec rhoncus venenatis. Duis pulvinar tellus vel quam " \ + "maximus imperdiet. Mauris eget nibh orci. Duis ut cursus nibh. Nulla sed commodo elit. " \ + "Suspendisse ac eros lacinia, mattis turpis at, porttitor justo. Vivamus molestie " \ + "tincidunt libero. Etiam porttitor lacus odio, at lobortis tortor scelerisque nec. " \ + "Nullam non ante vel nisi ultrices consectetur. Maecenas massa felis, tempor eget " \ + "malesuada eget, pretium eu sapien. Vivamus dapibus ante erat, non faucibus orci sodales " \ + "sit amet. Cras magna felis, sodales eget magna sed, eleifend rutrum ligula. Vivamus interdum " \ + "enim enim, eu facilisis tortor dignissim quis. Ut metus nulla, mattis non lorem et, " \ + "elementum ultrices orci. Quisque eleifend, arcu vitae ullamcorper pulvinar, ipsum ex " \ + "sodales arcu, eget consectetur mauris metus ac tortor. Donec id sem felis. Maur" + req.block1 = (6, 0, 1024) + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = None + expected.code = defines.Codes.CHANGED.number + expected.token = None + expected.payload = None + expected.location_path = "big" + + exchange7 = (req, expected) + self.current_mid += 1 + + self._test_plugtest([exchange1, exchange2, exchange3, exchange4, exchange5, exchange6, exchange7]) + + def test_options(self): + print("TEST_OPTIONS") + path = "/storage/new_res" + + req = Request() + req.code = defines.Codes.POST.number + req.uri_path = path + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + option = Option() + option.number = defines.OptionRegistry.ETAG.number + option.value = "test" + req.add_option(option) + req.del_option(option) + req.payload = "test" + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = self.current_mid + expected.code = defines.Codes.CREATED.number + expected.token = None + expected.payload = None + expected.location_path = "storage/new_res" + + exchange1 = (req, expected) + self.current_mid += 1 + + req = Request() + req.code = defines.Codes.POST.number + req.uri_path = path + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + option = Option() + option.number = defines.OptionRegistry.ETAG.number + option.value = "test" + req.add_option(option) + req.del_option_by_name("ETag") + req.payload = "test" + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = self.current_mid + expected.code = defines.Codes.CREATED.number + expected.token = None + expected.payload = None + expected.location_path = "storage/new_res" + + exchange2 = (req, expected) + self.current_mid += 1 + + req = Request() + req.code = defines.Codes.POST.number + req.uri_path = path + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + option = Option() + option.number = defines.OptionRegistry.ETAG.number + option.value = "test" + req.add_option(option) + del req.etag + req.payload = "test" + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = self.current_mid + expected.code = defines.Codes.CREATED.number + expected.token = None + expected.payload = None + expected.location_path = "storage/new_res" + + exchange3 = (req, expected) + self.current_mid += 1 + + self._test_with_client([exchange1, exchange2, exchange3]) + + def test_long_options(self): + """ + Test processing of options with extended length + """ + print("TEST_LONG_OPTIONS") + + path = "/storage/" + req = Request() + req.code = defines.Codes.GET.number + req.uri_path = path + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + option = Option() + # This option should be silently ignored by the server + # since it is not critical + option.number = defines.OptionRegistry.RM_MESSAGE_SWITCHING.number + option.value = "\1\1\1\1\0\0" + options = req.options + req.add_option(option) + req.payload = "test" + + expected = Response() + expected.type = defines.Types["ACK"] + expected.code = defines.Codes.CONTENT.number + expected.token = None + expected.payload = None + + exchange1 = (req, expected) + self.current_mid += 1 + + self._test_with_client([exchange1]) + + # This option (244) should be silently ignored by the server + req = ("\x40\x01\x01\x01\xd6\xe7\x01\x01\x01\x01\x00\x00", self.server_address) + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = None + expected.code = defines.Codes.NOT_FOUND.number + expected.token = None + expected.payload = None + + exchange21 = (req, expected) + self.current_mid += 1 + + # This option (245) should cause BAD REQUEST, as unrecognizable critical + req = ("\x40\x01\x01\x01\xd6\xe8\x01\x01\x01\x01\x00\x00", self.server_address) + + expected = Response() + expected.type = defines.Types["RST"] + expected._mid = None + expected.code = defines.Codes.BAD_REQUEST.number + + exchange22 = (req, expected) + self.current_mid += 1 + + # This option (65525) should cause BAD REQUEST, as unrecognizable critical + req = ("\x40\x01\x01\x01\xe6\xfe\xe8\x01\x01\x01\x01\x00\x00", self.server_address) + + expected = Response() + expected.type = defines.Types["RST"] + expected._mid = None + expected.code = defines.Codes.BAD_REQUEST.number + + exchange23 = (req, expected) + self.current_mid += 1 + + self._test_datagram([exchange21, exchange22, exchange23]) + + def test_content_type(self): + print("TEST_CONTENT_TYPE") + path = "/storage/new_res" + + req = Request() + req.code = defines.Codes.POST.number + req.uri_path = path + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + req.payload = "test" + req.content_type = defines.Content_types["application/xml"] + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = self.current_mid + expected.code = defines.Codes.CREATED.number + expected.token = None + expected.payload = None + expected.location_path = "storage/new_res" + + exchange1 = (req, expected) + self.current_mid += 1 + + req = Request() + req.code = defines.Codes.GET.number + req.uri_path = path + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = self.current_mid + expected.code = defines.Codes.CONTENT.number + expected.token = None + expected.payload = "Basic Resource" + + exchange2 = (req, expected) + self.current_mid += 1 + + req = Request() + req.code = defines.Codes.PUT.number + req.uri_path = path + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + req.payload = "test" + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = self.current_mid + expected.code = defines.Codes.CHANGED.number + expected.token = None + expected.payload = None + + exchange3 = (req, expected) + self.current_mid += 1 + + req = Request() + req.code = defines.Codes.GET.number + req.uri_path = path + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = self.current_mid + expected.code = defines.Codes.CONTENT.number + expected.token = None + expected.payload = "test" + + exchange4 = (req, expected) + self.current_mid += 1 + + req = Request() + req.code = defines.Codes.GET.number + req.uri_path = path + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + req.accept = defines.Content_types["application/xml"] + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = self.current_mid + expected.code = defines.Codes.CONTENT.number + expected.token = None + expected.payload = "test" + expected.content_type = defines.Content_types["application/xml"] + + exchange5 = (req, expected) + self.current_mid += 1 + + req = Request() + req.code = defines.Codes.GET.number + req.uri_path = path + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + req.accept = defines.Content_types["application/json"] + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = self.current_mid + expected.code = defines.Codes.NOT_ACCEPTABLE.number + expected.token = None + expected.payload = None + + exchange6 = (req, expected) + self.current_mid += 1 + + req = Request() + req.code = defines.Codes.GET.number + req.uri_path = "/xml" + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = self.current_mid + expected.code = defines.Codes.CONTENT.number + expected.token = None + expected.payload = "0" + + print((expected.pretty_print())) + + exchange7 = (req, expected) + self.current_mid += 1 + + req = Request() + req.code = defines.Codes.GET.number + req.uri_path = "/encoding" + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = self.current_mid + expected.code = defines.Codes.CONTENT.number + expected.token = None + expected.payload = "0" + + exchange8 = (req, expected) + self.current_mid += 1 + + req = Request() + req.code = defines.Codes.GET.number + req.uri_path = "/encoding" + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + req.accept = defines.Content_types["application/xml"] + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = self.current_mid + expected.code = defines.Codes.CONTENT.number + expected.token = None + expected.payload = "0" + expected.content_type = defines.Content_types["application/xml"] + + exchange9 = (req, expected) + self.current_mid += 1 + + req = Request() + req.code = defines.Codes.GET.number + req.uri_path = "/encoding" + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + req.accept = defines.Content_types["application/json"] + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = self.current_mid + expected.code = defines.Codes.CONTENT.number + expected.token = None + expected.payload = "{'value': '0'}" + expected.content_type = defines.Content_types["application/json"] + + exchange10 = (req, expected) + self.current_mid += 1 + + # self._test_with_client([exchange1, exchange2, exchange3, exchange4, exchange5, exchange6, exchange7, + # exchange8, exchange9, exchange10]) + self._test_with_client([exchange1, exchange2, exchange3, exchange4, exchange5]) + + def test_ETAG(self): + print("TEST_ETAG") + path = "/etag" + + req = Request() + req.code = defines.Codes.GET.number + req.uri_path = path + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = self.current_mid + expected.code = defines.Codes.CONTENT.number + expected.token = None + expected.payload = "ETag resource" + expected.etag = bytes("0", "utf-8") + + exchange1 = (req, expected) + self.current_mid += 1 + + req = Request() + req.code = defines.Codes.POST.number + req.uri_path = path + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + req.payload = "test" + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = self.current_mid + expected.code = defines.Codes.CHANGED.number + expected.token = None + expected.payload = None + expected.location_path = path + expected.etag = "1" + + exchange2 = (req, expected) + self.current_mid += 1 + + req = Request() + req.code = defines.Codes.GET.number + req.uri_path = path + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + req.etag = bytes("1", "utf-8") + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = self.current_mid + expected.code = defines.Codes.VALID.number + expected.token = None + expected.payload = "test" + expected.etag = bytes("1", "utf-8") + + exchange3 = (req, expected) + self.current_mid += 1 + + req = Request() + req.code = defines.Codes.PUT.number + req.uri_path = path + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + req.payload = "echo payload" + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = self.current_mid + expected.code = defines.Codes.CHANGED.number + expected.token = None + expected.payload = None + + exchange4 = (req, expected) + self.current_mid += 1 + + self._test_with_client([exchange1, exchange2, exchange3, exchange4]) + + def test_child(self): + print("TEST_CHILD") + path = "/child" + + req = Request() + req.code = defines.Codes.POST.number + req.uri_path = path + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + req.payload = "test" + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = self.current_mid + expected.code = defines.Codes.CREATED.number + expected.token = None + expected.payload = None + expected.location_path = path + + exchange1 = (req, expected) + self.current_mid += 1 + + req = Request() + req.code = defines.Codes.GET.number + req.uri_path = path + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = self.current_mid + expected.code = defines.Codes.CONTENT.number + expected.token = None + expected.payload = "test" + + exchange2 = (req, expected) + self.current_mid += 1 + + req = Request() + req.code = defines.Codes.PUT.number + req.uri_path = path + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + req.payload = "testPUT" + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = self.current_mid + expected.code = defines.Codes.CHANGED.number + expected.token = None + expected.payload = None + + exchange3 = (req, expected) + self.current_mid += 1 + + req = Request() + req.code = defines.Codes.DELETE.number + req.uri_path = path + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = self.current_mid + expected.code = defines.Codes.DELETED.number + expected.token = None + expected.payload = None + + exchange4 = (req, expected) + self.current_mid += 1 + + self._test_with_client([exchange1, exchange2, exchange3, exchange4]) + + def test_not_found(self): + print("TEST_not_found") + path = "/not_found" + + req = Request() + req.code = defines.Codes.GET.number + req.uri_path = path + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + req.token = 100 + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = self.current_mid + expected.code = defines.Codes.NOT_FOUND.number + expected.token = "100" + expected.payload = None + + exchange1 = (req, expected) + self.current_mid += 1 + + req = Request() + req.code = defines.Codes.POST.number + req.uri_path = path + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + req.payload = "test" + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = self.current_mid + expected.code = defines.Codes.METHOD_NOT_ALLOWED.number + expected.token = None + + exchange2 = (req, expected) + self.current_mid += 1 + + req = Request() + req.code = defines.Codes.PUT.number + req.uri_path = path + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + req.payload = "testPUT" + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = self.current_mid + expected.code = defines.Codes.NOT_FOUND.number + expected.token = None + expected.payload = None + + exchange3 = (req, expected) + self.current_mid += 1 + + req = Request() + req.code = defines.Codes.DELETE.number + req.uri_path = path + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = self.current_mid + expected.code = defines.Codes.NOT_FOUND.number + expected.token = None + expected.payload = None + + exchange4 = (req, expected) + self.current_mid += 1 + + self._test_with_client([exchange1, exchange2, exchange3, exchange4]) + + def test_invalid(self): + print("TEST_INVALID") + + # version + req = ("\x00\x01\x8c\xda", self.server_address) + + expected = Response() + expected.type = defines.Types["RST"] + expected._mid = None + expected.code = defines.Codes.BAD_REQUEST.number + + exchange1 = (req, expected) + + # version + req = ("\x40", self.server_address) + + expected = Response() + expected.type = defines.Types["RST"] + expected._mid = None + expected.code = defines.Codes.BAD_REQUEST.number + + exchange2 = (req, expected) + + # code + req = ("\x40\x05\x8c\xda", self.server_address) + + expected = Response() + expected.type = defines.Types["RST"] + expected._mid = None + expected.code = defines.Codes.BAD_REQUEST.number + + exchange3 = (req, expected) + + # option + req = ("\x40\x01\x8c\xda\x94", self.server_address) + + expected = Response() + expected.type = defines.Types["RST"] + expected._mid = None + expected.code = defines.Codes.BAD_REQUEST.number + + exchange4 = (req, expected) + + # payload marker + req = ("\x40\x02\x8c\xda\x75\x62\x61\x73\x69\x63\xff", self.server_address) + + expected = Response() + expected.type = defines.Types["RST"] + expected._mid = None + expected.code = defines.Codes.BAD_REQUEST.number + + exchange5 = (req, expected) + + self._test_datagram([exchange1, exchange2, exchange3, exchange4, exchange5]) + + def test_post_block_big_client(self): + print("TEST_POST_BLOCK_BIG_CLIENT") + path = "/big" + req = Request() + + req.code = defines.Codes.POST.number + req.uri_path = path + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + req.payload = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras sollicitudin fermentum ornare. " \ + "Cras accumsan tellus quis dui lacinia eleifend. Proin ultrices rutrum orci vitae luctus. " \ + "Nullam malesuada pretium elit, at aliquam odio vehicula in. Etiam nec maximus elit. " \ + "Etiam at erat ac ex ornare feugiat. Curabitur sed malesuada orci, id aliquet nunc. Phasellus " \ + "nec leo luctus, blandit lorem sit amet, interdum metus. Duis efficitur volutpat magna, ac " \ + "ultricies nibh aliquet sit amet. Etiam tempor egestas augue in hendrerit. Nunc eget augue " \ + "ultricies, dignissim lacus et, vulputate dolor. Nulla eros odio, fringilla vel massa ut, " \ + "facilisis cursus quam. Fusce faucibus lobortis congue. Fusce consectetur porta neque, id " \ + "sollicitudin velit maximus eu. Sed pharetra leo quam, vel finibus turpis cursus ac. " \ + "Aenean ac nisi massa. Cras commodo arcu nec ante tristique ullamcorper. Quisque eu hendrerit" \ + " urna. Cras fringilla eros ut nunc maximus, non porta nisl mollis. Aliquam in rutrum massa." \ + " Praesent tristique turpis dui, at ultricies lorem fermentum at. Vivamus sit amet ornare neque, " \ + "a imperdiet nisl. Quisque a iaculis libero, id tempus lacus. Aenean convallis est non justo " \ + "consectetur, a hendrerit enim consequat. In accumsan ante a egestas luctus. Etiam quis neque " \ + "nec eros vestibulum faucibus. Nunc viverra ipsum lectus, vel scelerisque dui dictum a. Ut orci " \ + "enim, ultrices a ultrices nec, pharetra in quam. Donec accumsan sit amet eros eget fermentum." \ + "Vivamus ut odio ac odio malesuada accumsan. Aenean vehicula diam at tempus ornare. Phasellus " \ + "dictum mauris a mi consequat, vitae mattis nulla fringilla. Ut laoreet tellus in nisl efficitur," \ + " a luctus justo tempus. Fusce finibus libero eget velit finibus iaculis. Morbi rhoncus purus " \ + "vel vestibulum ullamcorper. Sed ac metus in urna fermentum feugiat. Nulla nunc diam, sodales " \ + "aliquam mi id, varius porta nisl. Praesent vel nibh ac turpis rutrum laoreet at non odio. " \ + "Phasellus ut posuere mi. Suspendisse malesuada velit nec mauris convallis porta. Vivamus " \ + "sed ultrices sapien, at cras amet." + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = None + expected.code = defines.Codes.CHANGED.number + expected.token = None + expected.payload = None + + exchange1 = (req, expected) + self.current_mid += 1 + + self._test_with_client([exchange1]) + + def test_observe_client(self): + print("TEST_OBSERVE_CLIENT") + path = "/basic" + + req = Request() + req.code = defines.Codes.GET.number + req.uri_path = path + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + req.observe = 0 + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = None + expected.code = defines.Codes.CONTENT.number + expected.token = None + expected.payload = None + expected.observe = 1 + + exchange1 = (req, expected) + + req = Message() + req.code = defines.Codes.EMPTY.number + req.uri_path = path + req.type = defines.Types["RST"] + req._mid = self.current_mid + req.destination = self.server_address + + exchange2 = (req, None) + self.current_mid += 1 + + self._test_with_client_observe([exchange1, exchange2]) + +if __name__ == '__main__': + unittest.main() + diff --git a/coverage_testIPv6.py b/coverage_testIPv6.py new file mode 100644 index 0000000..cdbc876 --- /dev/null +++ b/coverage_testIPv6.py @@ -0,0 +1,164 @@ +from queue import Queue +import random +import threading +import unittest +from coapclient import HelperClient +from coapserver import CoAPServer +from coapthon import defines +from coapthon.messages.option import Option +from coapthon.messages.request import Request +from coapthon.messages.response import Response + +__author__ = 'Giacomo Tanganelli' +__version__ = "2.0" + + +class Tests(unittest.TestCase): + + def setUp(self): + self.server_address = ("::1", 5683) + self.current_mid = random.randint(1, 1000) + self.server_mid = random.randint(1000, 2000) + self.server = CoAPServer("::1", 5683) + self.server_thread = threading.Thread(target=self.server.listen, args=(10,)) + self.server_thread.start() + self.queue = Queue() + + def tearDown(self): + self.server.close() + self.server_thread.join(timeout=25) + self.server = None + + def _test_with_client(self, message_list): # pragma: no cover + client = HelperClient(self.server_address) + for message, expected in message_list: + if message is not None: + received_message = client.send_request(message) + if expected is not None: + if expected.type is not None: + self.assertEqual(received_message.type, expected.type) + if expected.mid is not None: + self.assertEqual(received_message.mid, expected.mid) + self.assertEqual(received_message.code, expected.code) + if expected.source is not None: + self.assertEqual(received_message.source, self.server_address) + if expected.token is not None: + self.assertEqual(received_message.token, expected.token) + if expected.payload is not None: + self.assertEqual(received_message.payload, expected.payload) + if expected.options: + self.assertEqual(len(received_message.options), len(expected.options)) + for o in expected.options: + assert isinstance(o, Option) + option_value = getattr(expected, o.name.lower().replace("-", "_")) + option_value_rec = getattr(received_message, o.name.lower().replace("-", "_")) + self.assertEqual(option_value, option_value_rec) + client.stop() + + def _test_with_client_observe(self, message_list): # pragma: no cover + client = HelperClient(self.server_address) + for message, expected in message_list: + if message is not None: + client.send_request(message, self.client_callback) + if expected is not None: + received_message = self.queue.get() + if expected.type is not None: + self.assertEqual(received_message.type, expected.type) + if expected.mid is not None: + self.assertEqual(received_message.mid, expected.mid) + self.assertEqual(received_message.code, expected.code) + if expected.source is not None: + self.assertEqual(received_message.source, self.server_address) + if expected.token is not None: + self.assertEqual(received_message.token, expected.token) + if expected.payload is not None: + self.assertEqual(received_message.payload, expected.payload) + if expected.options: + self.assertEqual(len(received_message.options), len(expected.options)) + for o in expected.options: + assert isinstance(o, Option) + option_value = getattr(expected, o.name.lower().replace("-", "_")) + option_value_rec = getattr(received_message, o.name.lower().replace("-", "_")) + self.assertEqual(option_value, option_value_rec) + client.stop() + + def client_callback(self, response): + print("Callback") + self.queue.put(response) + + def test_not_allowed(self): + print("TEST_NOT_ALLOWED") + path = "/void" + req = Request() + req.code = defines.Codes.GET.number + req.uri_path = path + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = self.current_mid + expected.code = defines.Codes.METHOD_NOT_ALLOWED.number + expected.token = None + + exchange1 = (req, expected) + + self.current_mid += 1 + + req = Request() + req.code = defines.Codes.POST.number + req.uri_path = path + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = self.current_mid + expected.code = defines.Codes.METHOD_NOT_ALLOWED.number + expected.token = None + + exchange2 = (req, expected) + + self.current_mid += 1 + + req = Request() + req.code = defines.Codes.PUT.number + req.uri_path = path + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = self.current_mid + expected.code = defines.Codes.METHOD_NOT_ALLOWED.number + expected.token = None + + exchange3 = (req, expected) + + self.current_mid += 1 + + req = Request() + req.code = defines.Codes.DELETE.number + req.uri_path = path + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = self.current_mid + expected.code = defines.Codes.METHOD_NOT_ALLOWED.number + expected.token = None + + exchange4 = (req, expected) + + self.current_mid += 1 + self._test_with_client([exchange1, exchange2, exchange3, exchange4]) + + +if __name__ == '__main__': + unittest.main() + diff --git a/coverage_test_advanced.py b/coverage_test_advanced.py new file mode 100644 index 0000000..31b4657 --- /dev/null +++ b/coverage_test_advanced.py @@ -0,0 +1,214 @@ +import threading +import unittest +from queue import Queue +import random + +from coapserver import CoAPServer +from coapthon import defines +from coapthon.client.helperclient import HelperClient +from coapthon.messages.option import Option +from coapthon.messages.request import Request +from coapthon.messages.response import Response + + +class Tests(unittest.TestCase): + + def setUp(self): + self.server_address = ("127.0.0.1", 5683) + self.current_mid = random.randint(1, 1000) + self.server_mid = random.randint(1000, 2000) + self.server = CoAPServer("127.0.0.1", 5683) + self.server_thread = threading.Thread(target=self.server.listen, args=(10,)) + self.server_thread.start() + self.queue = Queue() + + def tearDown(self): + self.server.close() + self.server_thread.join(timeout=25) + self.server = None + + def _test_with_client(self, message_list): # pragma: no cover + client = HelperClient(self.server_address) + for message, expected in message_list: + if message is not None: + received_message = client.send_request(message) + if expected is not None: + + if expected.type is not None: + self.assertEqual(received_message.type, expected.type) + if expected.mid is not None: + self.assertEqual(received_message.mid, expected.mid) + self.assertEqual(received_message.code, expected.code) + if expected.source is not None: + self.assertEqual(received_message.source, self.server_address) + if expected.token is not None: + self.assertEqual(received_message.token, expected.token) + if expected.payload is not None: + self.assertEqual(received_message.payload, expected.payload) + if expected.options: + self.assertEqual(len(received_message.options), len(expected.options)) + for o in expected.options: + assert isinstance(o, Option) + option_value = getattr(expected, o.name.lower().replace("-", "_")) + option_value_rec = getattr(received_message, o.name.lower().replace("-", "_")) + self.assertEqual(option_value, option_value_rec) + client.stop() + + def client_callback(self, response): + print("Callback") + self.queue.put(response) + + def test_advanced(self): + print("TEST_ADVANCED") + path = "/advanced" + req = Request() + req.code = defines.Codes.GET.number + req.uri_path = path + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = self.current_mid + expected.code = defines.Codes.CONTENT.number + expected.max_age = 20 + expected.token = None + + exchange1 = (req, expected) + + self.current_mid += 1 + + req = Request() + req.code = defines.Codes.POST.number + req.uri_path = path + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = self.current_mid + expected.code = defines.Codes.CREATED.number + expected.payload = "Response changed through POST" + expected.token = None + + exchange2 = (req, expected) + + self.current_mid += 1 + + req = Request() + req.code = defines.Codes.PUT.number + req.uri_path = path + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = self.current_mid + expected.code = defines.Codes.CHANGED.number + expected.payload = "Response changed through PUT" + expected.token = None + + exchange3 = (req, expected) + + self.current_mid += 1 + + req = Request() + req.code = defines.Codes.DELETE.number + req.uri_path = path + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = self.current_mid + expected.code = defines.Codes.DELETED.number + expected.payload = "Response deleted" + expected.token = None + + exchange4 = (req, expected) + + self.current_mid += 1 + self._test_with_client([exchange1, exchange2, exchange3, exchange4]) + + def test_advanced_separate(self): + print("TEST_ADVANCED_SEPARATE") + path = "/advancedSeparate" + req = Request() + req.code = defines.Codes.GET.number + req.uri_path = path + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + + expected = Response() + expected.type = defines.Types["CON"] + expected._mid = None + expected.code = defines.Codes.CONTENT.number + expected.max_age = 20 + expected.token = None + + exchange1 = (req, expected) + + self.current_mid += 1 + + req = Request() + req.code = defines.Codes.POST.number + req.uri_path = path + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + + expected = Response() + expected.type = defines.Types["CON"] + expected._mid = None + expected.code = defines.Codes.CREATED.number + expected.payload = "Response changed through POST" + expected.token = None + + exchange2 = (req, expected) + + self.current_mid += 1 + + req = Request() + req.code = defines.Codes.PUT.number + req.uri_path = path + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + + expected = Response() + expected.type = defines.Types["CON"] + expected._mid = None + expected.code = defines.Codes.CHANGED.number + expected.payload = "Response changed through PUT" + expected.token = None + + exchange3 = (req, expected) + + self.current_mid += 1 + + req = Request() + req.code = defines.Codes.DELETE.number + req.uri_path = path + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + + expected = Response() + expected.type = defines.Types["CON"] + expected._mid = None + expected.code = defines.Codes.DELETED.number + expected.payload = "Response deleted" + expected.token = None + + exchange4 = (req, expected) + + self.current_mid += 1 + self._test_with_client([exchange1, exchange2, exchange3, exchange4]) + +if __name__ == '__main__': + unittest.main() diff --git a/coverage_test_multicast.py b/coverage_test_multicast.py new file mode 100644 index 0000000..994f521 --- /dev/null +++ b/coverage_test_multicast.py @@ -0,0 +1,165 @@ +from queue import Queue +import random +import threading +import unittest +from coapclient import HelperClient +from coapserver import CoAPServer +from coapthon import defines +from coapthon.messages.message import Message +from coapthon.messages.option import Option +from coapthon.messages.request import Request +from coapthon.messages.response import Response + +__author__ = 'Giacomo Tanganelli' +__version__ = "2.0" + + +class Tests(unittest.TestCase): + + def setUp(self): + self.server_address = (defines.ALL_COAP_NODES, 5683) + self.current_mid = random.randint(1, 1000) + self.server_mid = random.randint(1000, 2000) + self.server = CoAPServer("0.0.0.0", 5683, multicast=True) + self.server_thread = threading.Thread(target=self.server.listen, args=(10,)) + self.server_thread.start() + self.queue = Queue() + + def tearDown(self): + self.server.close() + self.server_thread.join(timeout=25) + self.server = None + + def _test_with_client(self, message_list): # pragma: no cover + client = HelperClient(self.server_address) + for message, expected in message_list: + if message is not None: + received_message = client.send_request(message) + if expected is not None: + if expected.type is not None: + self.assertEqual(received_message.type, expected.type) + if expected.mid is not None: + self.assertEqual(received_message.mid, expected.mid) + self.assertEqual(received_message.code, expected.code) + if expected.source is not None: + self.assertEqual(received_message.source, self.server_address) + if expected.token is not None: + self.assertEqual(received_message.token, expected.token) + if expected.payload is not None: + self.assertEqual(received_message.payload, expected.payload) + if expected.options: + self.assertEqual(len(received_message.options), len(expected.options)) + for o in expected.options: + assert isinstance(o, Option) + option_value = getattr(expected, o.name.lower().replace("-", "_")) + option_value_rec = getattr(received_message, o.name.lower().replace("-", "_")) + self.assertEqual(option_value, option_value_rec) + client.stop() + + def _test_with_client_observe(self, message_list): # pragma: no cover + client = HelperClient(self.server_address) + for message, expected in message_list: + if message is not None: + client.send_request(message, self.client_callback) + if expected is not None: + received_message = self.queue.get() + if expected.type is not None: + self.assertEqual(received_message.type, expected.type) + if expected.mid is not None: + self.assertEqual(received_message.mid, expected.mid) + self.assertEqual(received_message.code, expected.code) + if expected.source is not None: + self.assertEqual(received_message.source, self.server_address) + if expected.token is not None: + self.assertEqual(received_message.token, expected.token) + if expected.payload is not None: + self.assertEqual(received_message.payload, expected.payload) + if expected.options: + self.assertEqual(len(received_message.options), len(expected.options)) + for o in expected.options: + assert isinstance(o, Option) + option_value = getattr(expected, o.name.lower().replace("-", "_")) + option_value_rec = getattr(received_message, o.name.lower().replace("-", "_")) + self.assertEqual(option_value, option_value_rec) + client.stop() + + def client_callback(self, response): + print("Callback") + self.queue.put(response) + + def test_not_allowed(self): + print("TEST_NOT_ALLOWED") + path = "/void" + req = Request() + req.code = defines.Codes.GET.number + req.uri_path = path + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = self.current_mid + expected.code = defines.Codes.METHOD_NOT_ALLOWED.number + expected.token = None + + exchange1 = (req, expected) + + self.current_mid += 1 + + req = Request() + req.code = defines.Codes.POST.number + req.uri_path = path + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = self.current_mid + expected.code = defines.Codes.METHOD_NOT_ALLOWED.number + expected.token = None + + exchange2 = (req, expected) + + self.current_mid += 1 + + req = Request() + req.code = defines.Codes.PUT.number + req.uri_path = path + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = self.current_mid + expected.code = defines.Codes.METHOD_NOT_ALLOWED.number + expected.token = None + + exchange3 = (req, expected) + + self.current_mid += 1 + + req = Request() + req.code = defines.Codes.DELETE.number + req.uri_path = path + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = self.current_mid + expected.code = defines.Codes.METHOD_NOT_ALLOWED.number + expected.token = None + + exchange4 = (req, expected) + + self.current_mid += 1 + self._test_with_client([exchange1, exchange2, exchange3, exchange4]) + + +if __name__ == '__main__': + unittest.main() + diff --git a/coverage_test_multicast_ipv6.py b/coverage_test_multicast_ipv6.py new file mode 100644 index 0000000..ba6a87d --- /dev/null +++ b/coverage_test_multicast_ipv6.py @@ -0,0 +1,165 @@ +from queue import Queue +import random +import threading +import unittest +from coapclient import HelperClient +from coapserver import CoAPServer +from coapthon import defines +from coapthon.messages.message import Message +from coapthon.messages.option import Option +from coapthon.messages.request import Request +from coapthon.messages.response import Response + +__author__ = 'Giacomo Tanganelli' +__version__ = "2.0" + + +class Tests(unittest.TestCase): + + def setUp(self): + self.server_address = (defines.ALL_COAP_NODES_IPV6, 5683) + self.current_mid = random.randint(1, 1000) + self.server_mid = random.randint(1000, 2000) + self.server = CoAPServer("::1", 5683, multicast=True) + self.server_thread = threading.Thread(target=self.server.listen, args=(10,)) + self.server_thread.start() + self.queue = Queue() + + def tearDown(self): + self.server.close() + self.server_thread.join(timeout=25) + self.server = None + + def _test_with_client(self, message_list): # pragma: no cover + client = HelperClient(self.server_address) + for message, expected in message_list: + if message is not None: + received_message = client.send_request(message) + if expected is not None: + if expected.type is not None: + self.assertEqual(received_message.type, expected.type) + if expected.mid is not None: + self.assertEqual(received_message.mid, expected.mid) + self.assertEqual(received_message.code, expected.code) + if expected.source is not None: + self.assertEqual(received_message.source, self.server_address) + if expected.token is not None: + self.assertEqual(received_message.token, expected.token) + if expected.payload is not None: + self.assertEqual(received_message.payload, expected.payload) + if expected.options: + self.assertEqual(len(received_message.options), len(expected.options)) + for o in expected.options: + assert isinstance(o, Option) + option_value = getattr(expected, o.name.lower().replace("-", "_")) + option_value_rec = getattr(received_message, o.name.lower().replace("-", "_")) + self.assertEqual(option_value, option_value_rec) + client.stop() + + def _test_with_client_observe(self, message_list): # pragma: no cover + client = HelperClient(self.server_address) + for message, expected in message_list: + if message is not None: + client.send_request(message, self.client_callback) + if expected is not None: + received_message = self.queue.get() + if expected.type is not None: + self.assertEqual(received_message.type, expected.type) + if expected.mid is not None: + self.assertEqual(received_message.mid, expected.mid) + self.assertEqual(received_message.code, expected.code) + if expected.source is not None: + self.assertEqual(received_message.source, self.server_address) + if expected.token is not None: + self.assertEqual(received_message.token, expected.token) + if expected.payload is not None: + self.assertEqual(received_message.payload, expected.payload) + if expected.options: + self.assertEqual(len(received_message.options), len(expected.options)) + for o in expected.options: + assert isinstance(o, Option) + option_value = getattr(expected, o.name.lower().replace("-", "_")) + option_value_rec = getattr(received_message, o.name.lower().replace("-", "_")) + self.assertEqual(option_value, option_value_rec) + client.stop() + + def client_callback(self, response): + print("Callback") + self.queue.put(response) + + def test_not_allowed(self): + print("TEST_NOT_ALLOWED") + path = "/void" + req = Request() + req.code = defines.Codes.GET.number + req.uri_path = path + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = self.current_mid + expected.code = defines.Codes.METHOD_NOT_ALLOWED.number + expected.token = None + + exchange1 = (req, expected) + + self.current_mid += 1 + + req = Request() + req.code = defines.Codes.POST.number + req.uri_path = path + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = self.current_mid + expected.code = defines.Codes.METHOD_NOT_ALLOWED.number + expected.token = None + + exchange2 = (req, expected) + + self.current_mid += 1 + + req = Request() + req.code = defines.Codes.PUT.number + req.uri_path = path + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = self.current_mid + expected.code = defines.Codes.METHOD_NOT_ALLOWED.number + expected.token = None + + exchange3 = (req, expected) + + self.current_mid += 1 + + req = Request() + req.code = defines.Codes.DELETE.number + req.uri_path = path + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = self.current_mid + expected.code = defines.Codes.METHOD_NOT_ALLOWED.number + expected.token = None + + exchange4 = (req, expected) + + self.current_mid += 1 + self._test_with_client([exchange1, exchange2, exchange3, exchange4]) + + +if __name__ == '__main__': + unittest.main() + diff --git a/coverage_test_proxy.py b/coverage_test_proxy.py new file mode 100644 index 0000000..cf27a66 --- /dev/null +++ b/coverage_test_proxy.py @@ -0,0 +1,1421 @@ +from queue import Queue +import random +import socket +import threading +import unittest +from coapclient import HelperClient +from coapforwardproxy import CoAPForwardProxy +from coapserver import CoAPServer +from coapthon import defines +from coapthon.messages.option import Option +from coapthon.messages.request import Request +from coapthon.messages.response import Response +from coapthon.serializer import Serializer + +__author__ = 'Giacomo Tanganelli' +__version__ = "2.0" + + +class Tests(unittest.TestCase): + + def setUp(self): + self.server_address = ("127.0.0.1", 5683) + self.current_mid = random.randint(1, 1000) + self.server_mid = random.randint(1000, 2000) + self.server = CoAPServer("127.0.0.1", 5684) + self.server_thread = threading.Thread(target=self.server.listen, args=(10,)) + self.server_thread.start() + self.proxy = CoAPForwardProxy("127.0.0.1", 5683) + self.proxy_thread = threading.Thread(target=self.proxy.listen, args=(10,)) + self.proxy_thread.start() + self.queue = Queue() + + def tearDown(self): + self.server.close() + self.server_thread.join(timeout=25) + self.server = None + self.proxy.close() + self.proxy_thread.join(timeout=25) + self.proxy = None + + def _test_with_client(self, message_list): # pragma: no cover + client = HelperClient(self.server_address) + for message, expected in message_list: + if message is not None: + received_message = client.send_request(message) + if expected is not None: + if expected.type is not None: + self.assertEqual(received_message.type, expected.type) + if expected.mid is not None: + self.assertEqual(received_message.mid, expected.mid) + self.assertEqual(received_message.code, expected.code) + if expected.source is not None: + self.assertEqual(received_message.source, self.server_address) + if expected.token is not None: + self.assertEqual(received_message.token, expected.token) + if expected.payload is not None: + self.assertEqual(received_message.payload, expected.payload) + if expected.options: + self.assertEqual(len(received_message.options), len(expected.options)) + for o in expected.options: + assert isinstance(o, Option) + option_value = getattr(expected, o.name.lower().replace("-", "_")) + option_value_rec = getattr(received_message, o.name.lower().replace("-", "_")) + self.assertEqual(option_value, option_value_rec) + client.stop() + + def _test_with_client_observe(self, message_list): # pragma: no cover + client = HelperClient(self.server_address) + for message, expected in message_list: + if message is not None: + client.send_request(message, self.client_callback) + if expected is not None: + received_message = self.queue.get() + if expected.type is not None: + self.assertEqual(received_message.type, expected.type) + if expected.mid is not None: + self.assertEqual(received_message.mid, expected.mid) + self.assertEqual(received_message.code, expected.code) + if expected.source is not None: + self.assertEqual(received_message.source, self.server_address) + if expected.token is not None: + self.assertEqual(received_message.token, expected.token) + if expected.payload is not None: + self.assertEqual(received_message.payload, expected.payload) + if expected.options: + self.assertEqual(len(received_message.options), len(expected.options)) + for o in expected.options: + assert isinstance(o, Option) + option_value = getattr(expected, o.name.lower().replace("-", "_")) + option_value_rec = getattr(received_message, o.name.lower().replace("-", "_")) + self.assertEqual(option_value, option_value_rec) + client.stop() + + def client_callback(self, response): + print("Callback") + self.queue.put(response) + + def _test_plugtest(self, message_list): # pragma: no cover + serializer = Serializer() + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + for message, expected in message_list: + if message is not None: + datagram = serializer.serialize(message) + sock.sendto(datagram, message.destination) + if expected is not None: + datagram, source = sock.recvfrom(4096) + received_message = serializer.deserialize(datagram, source) + print((received_message.pretty_print())) + print((expected.pretty_print())) + if expected.type is not None: + self.assertEqual(received_message.type, expected.type) + if expected.mid is not None: + self.assertEqual(received_message.mid, expected.mid) + self.assertEqual(received_message.code, expected.code) + if expected.source is not None: + self.assertEqual(received_message.source, source) + if expected.token is not None: + self.assertEqual(received_message.token, expected.token) + if expected.payload is not None: + self.assertEqual(received_message.payload, expected.payload) + if expected.options is not None: + self.assertEqual(received_message.options, expected.options) + for o in expected.options: + assert isinstance(o, Option) + option_value = getattr(expected, o.name.lower().replace("-", "_")) + option_value_rec = getattr(received_message, o.name.lower().replace("-", "_")) + self.assertEqual(option_value, option_value_rec) + sock.close() + + def _test_datagram(self, message_list): # pragma: no cover + serializer = Serializer() + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + for message, expected in message_list: + if message is not None: + datagram, destination = message + sock.sendto(datagram, destination) + if expected is not None: + datagram, source = sock.recvfrom(4096) + received_message = serializer.deserialize(datagram, source) + if expected.type is not None: + self.assertEqual(received_message.type, expected.type) + if expected.mid is not None: + self.assertEqual(received_message.mid, expected.mid) + self.assertEqual(received_message.code, expected.code) + if expected.source is not None: + self.assertEqual(received_message.source, source) + if expected.token is not None: + self.assertEqual(received_message.token, expected.token) + if expected.payload is not None: + self.assertEqual(received_message.payload, expected.payload) + if expected.options is not None: + self.assertEqual(received_message.options, expected.options) + for o in expected.options: + assert isinstance(o, Option) + option_value = getattr(expected, o.name.lower().replace("-", "_")) + option_value_rec = getattr(received_message, o.name.lower().replace("-", "_")) + self.assertEqual(option_value, option_value_rec) + sock.close() + + def test_get_forward(self): + print("TEST_GET_FORWARD") + path = "/basic" + req = Request() + req.code = defines.Codes.GET.number + req.uri_path = path + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + req.proxy_uri = "coap://127.0.0.1:5684/basic" + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = self.current_mid + expected.code = defines.Codes.CONTENT.number + expected.token = None + expected.payload = "Basic Resource" + + exchange1 = (req, expected) + + self.current_mid += 1 + + self._test_with_client([exchange1]) + + # def test_separate(self): + # print "TEST_SEPARATE" + # path = "/separate" + # req = Request() + # req.code = defines.Codes.GET.number + # req.uri_path = path + # req.type = defines.Types["CON"] + # req._mid = self.current_mid + # req.destination = self.server_address + # + # expected = Response() + # expected.type = defines.Types["CON"] + # expected._mid = None + # expected.code = defines.Codes.CONTENT.number + # expected.token = None + # expected.max_age = 60 + # + # exchange1 = (req, expected) + # + # self.current_mid += 1 + # + # req = Request() + # req.code = defines.Codes.POST.number + # req.uri_path = path + # req.type = defines.Types["CON"] + # req._mid = self.current_mid + # req.destination = self.server_address + # req.payload = "POST" + # + # expected = Response() + # expected.type = defines.Types["CON"] + # expected._mid = None + # expected.code = defines.Codes.CREATED.number + # expected.token = None + # expected.options = None + # + # exchange2 = (req, expected) + # + # self.current_mid += 1 + # + # req = Request() + # req.code = defines.Codes.PUT.number + # req.uri_path = path + # req.type = defines.Types["CON"] + # req._mid = self.current_mid + # req.destination = self.server_address + # req.payload = "PUT" + # + # expected = Response() + # expected.type = defines.Types["CON"] + # expected._mid = None + # expected.code = defines.Codes.CHANGED.number + # expected.token = None + # expected.options = None + # + # exchange3 = (req, expected) + # + # self.current_mid += 1 + # + # req = Request() + # req.code = defines.Codes.DELETE.number + # req.uri_path = path + # req.type = defines.Types["CON"] + # req._mid = self.current_mid + # req.destination = self.server_address + # + # expected = Response() + # expected.type = defines.Types["CON"] + # expected._mid = None + # expected.code = defines.Codes.DELETED.number + # expected.token = None + # + # exchange4 = (req, expected) + # + # self.current_mid += 1 + # self._test_with_client([exchange1, exchange2, exchange3, exchange4]) + # + def test_post(self): + print("TEST_POST") + path = "/storage/new_res?id=1" + req = Request() + + req.code = defines.Codes.POST.number + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + req.payload = "test" + req.add_if_none_match() + req.proxy_uri = "coap://127.0.0.1:5684/storage/new_res?id=1" + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = self.current_mid + expected.code = defines.Codes.CREATED.number + expected.token = None + expected.payload = None + expected.location_path = "storage/new_res" + expected.location_query = "id=1" + + exchange1 = (req, expected) + self.current_mid += 1 + + req = Request() + req.code = defines.Codes.GET.number + req.proxy_uri = "coap://127.0.0.1:5684/storage/new_res" + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + req.if_match = ["test", "not"] + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = self.current_mid + expected.code = defines.Codes.CONTENT.number + expected.token = None + expected.payload = "test" + + exchange2 = (req, expected) + self.current_mid += 1 + + req = Request() + req.code = defines.Codes.PUT.number + req.proxy_uri = "coap://127.0.0.1:5684/storage/new_res" + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + req.if_match = ["not"] + req.payload = "not" + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = self.current_mid + expected.code = defines.Codes.PRECONDITION_FAILED.number + expected.token = None + + exchange3 = (req, expected) + self.current_mid += 1 + + req = Request() + req.code = defines.Codes.POST.number + req.proxy_uri = "coap://127.0.0.1:5684/storage/new_res" + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + req.if_match = ["not"] + req.payload = "not" + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = self.current_mid + expected.code = defines.Codes.PRECONDITION_FAILED.number + expected.token = None + + exchange4 = (req, expected) + self.current_mid += 1 + + req = Request() + req.code = defines.Codes.PUT.number + req.proxy_uri = "coap://127.0.0.1:5684/storage/new_res" + req._mid = self.current_mid + req.destination = self.server_address + req.add_if_none_match() + req.payload = "not" + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = self.current_mid + expected.code = defines.Codes.PRECONDITION_FAILED.number + expected.token = None + + exchange5 = (req, expected) + self.current_mid += 1 + + self._test_with_client([exchange1, exchange2, exchange3, exchange4, exchange5]) + + def test_post_block(self): + print("TEST_POST_BLOCK") + req = Request() + + req.code = defines.Codes.POST.number + req.proxy_uri = "coap://127.0.0.1:5684/storage/new_res" + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + req.payload = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras sollicitudin fermentum ornare. " \ + "Cras accumsan tellus quis dui lacinia eleifend. Proin ultrices rutrum orci vitae luctus. " \ + "Nullam malesuada pretium elit, at aliquam odio vehicula in. Etiam nec maximus elit. " \ + "Etiam at erat ac ex ornare feugiat. Curabitur sed malesuada orci, id aliquet nunc. Phasellus " \ + "nec leo luctus, blandit lorem sit amet, interdum metus. Duis efficitur volutpat magna, ac " \ + "ultricies nibh aliquet sit amet. Etiam tempor egestas augue in hendrerit. Nunc eget augue " \ + "ultricies, dignissim lacus et, vulputate dolor. Nulla eros odio, fringilla vel massa ut, " \ + "facilisis cursus quam. Fusce faucibus lobortis congue. Fusce consectetur porta neque, id " \ + "sollicitudin velit maximus eu. Sed pharetra leo quam, vel finibus turpis cursus ac. " \ + "Aenean ac nisi massa. Cras commodo arcu nec ante tristique ullamcorper. Quisque eu hendrerit" \ + " urna. Cras fringilla eros ut nunc maximus, non porta nisl mollis. Aliquam in rutrum massa." \ + " Praesent tristique turpis dui, at ultri" + req.block1 = (1, 1, 1024) + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = None + expected.code = defines.Codes.REQUEST_ENTITY_INCOMPLETE.number + expected.token = None + expected.payload = None + + exchange1 = (req, expected) + self.current_mid += 1 + + req = Request() + req.code = defines.Codes.POST.number + req.proxy_uri = "coap://127.0.0.1:5684/storage/new_res" + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + req.payload = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras sollicitudin fermentum ornare. " \ + "Cras accumsan tellus quis dui lacinia eleifend. Proin ultrices rutrum orci vitae luctus. " \ + "Nullam malesuada pretium elit, at aliquam odio vehicula in. Etiam nec maximus elit. " \ + "Etiam at erat ac ex ornare feugiat. Curabitur sed malesuada orci, id aliquet nunc. Phasellus " \ + "nec leo luctus, blandit lorem sit amet, interdum metus. Duis efficitur volutpat magna, ac " \ + "ultricies nibh aliquet sit amet. Etiam tempor egestas augue in hendrerit. Nunc eget augue " \ + "ultricies, dignissim lacus et, vulputate dolor. Nulla eros odio, fringilla vel massa ut, " \ + "facilisis cursus quam. Fusce faucibus lobortis congue. Fusce consectetur porta neque, id " \ + "sollicitudin velit maximus eu. Sed pharetra leo quam, vel finibus turpis cursus ac. " \ + "Aenean ac nisi massa. Cras commodo arcu nec ante tristique ullamcorper. Quisque eu hendrerit" \ + " urna. Cras fringilla eros ut nunc maximus, non porta nisl mollis. Aliquam in rutrum massa." \ + " Praesent tristique turpis dui, at ultri" + req.block1 = (0, 1, 1024) + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = None + expected.code = defines.Codes.CONTINUE.number + expected.token = None + expected.payload = None + expected.block1 = (0, 1, 1024) + + exchange2 = (req, expected) + self.current_mid += 1 + + req = Request() + req.code = defines.Codes.POST.number + req.proxy_uri = "coap://127.0.0.1:5684/storage/new_res" + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + req.payload = "a imperdiet nisl. Quisque a iaculis libero, id tempus lacus. Aenean convallis est non justo " \ + "consectetur, a hendrerit enim consequat. In accumsan ante a egestas luctus. Etiam quis neque " \ + "nec eros vestibulum faucibus. Nunc viverra ipsum lectus, vel scelerisque dui dictum a. Ut orci " \ + "enim, ultrices a ultrices nec, pharetra in quam. Donec accumsan sit amet eros eget fermentum." + req.block1 = (1, 1, 64) + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = None + expected.code = defines.Codes.CONTINUE.number + expected.token = None + expected.payload = None + expected.block1 = (1, 1, 64) + + exchange3 = (req, expected) + self.current_mid += 1 + + req = Request() + req.code = defines.Codes.POST.number + req.proxy_uri = "coap://127.0.0.1:5684/storage/new_res" + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + req.payload = "a imperdiet nisl. Quisque a iaculis libero, id tempus lacus. Aenean convallis est non justo " \ + "consectetur, a hendrerit enim consequat. In accumsan ante a egestas luctus. Etiam quis neque " \ + "nec eros vestibulum faucibus. Nunc viverra ipsum lectus, vel scelerisque dui dictum a. Ut orci " \ + "enim, ultrices a ultrices nec, pharetra in quam. Donec accumsan sit amet eros eget fermentum." + req.block1 = (3, 1, 64) + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = None + expected.code = defines.Codes.REQUEST_ENTITY_INCOMPLETE.number + expected.token = None + expected.payload = None + + exchange4 = (req, expected) + self.current_mid += 1 + + req = Request() + req.code = defines.Codes.POST.number + req.proxy_uri = "coap://127.0.0.1:5684/storage/new_res" + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + req.payload = "a imperdiet nisl. Quisque a iaculis libero, id tempus lacus. Aenean convallis est non justo " \ + "consectetur, a hendrerit enim consequat. In accumsan ante a egestas luctus. Etiam quis neque " \ + "nec eros vestibulum faucibus. Nunc viverra ipsum lectus, vel scelerisque dui dictum a. Ut orci " \ + "enim, ultrices a ultrices nec, pharetra in quam. Donec accumsan sit amet eros eget fermentum." + req.block1 = (2, 0, 64) + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = None + expected.code = defines.Codes.CREATED.number + expected.token = None + expected.payload = None + expected.location_path = "storage/new_res" + + exchange5 = (req, expected) + self.current_mid += 1 + + self._test_plugtest([exchange1, exchange2, exchange3, exchange4, exchange5]) + + def test_get_block(self): + print("TEST_GET_BLOCK") + + req = Request() + req.code = defines.Codes.GET.number + req.proxy_uri = "coap://127.0.0.1:5684/big" + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + req.payload = None + req.block2 = (0, 0, 512) + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = None + expected.code = defines.Codes.CONTENT.number + expected.token = None + expected.payload = None + expected.block2 = (0, 1, 512) + + exchange1 = (req, expected) + self.current_mid += 1 + + req = Request() + req.code = defines.Codes.GET.number + req.proxy_uri = "coap://127.0.0.1:5684/big" + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + req.payload = None + req.block2 = (1, 0, 256) + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = None + expected.code = defines.Codes.CONTENT.number + expected.token = None + expected.payload = None + expected.block2 = (1, 1, 256) + + exchange2 = (req, expected) + self.current_mid += 1 + + req = Request() + req.code = defines.Codes.GET.number + req.proxy_uri = "coap://127.0.0.1:5684/big" + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + req.payload = None + req.block2 = (2, 0, 128) + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = None + expected.code = defines.Codes.CONTENT.number + expected.token = None + expected.payload = None + expected.block2 = (2, 1, 128) + + exchange3 = (req, expected) + self.current_mid += 1 + + req = Request() + req.code = defines.Codes.GET.number + req.proxy_uri = "coap://127.0.0.1:5684/big" + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + req.payload = None + req.block2 = (3, 0, 64) + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = None + expected.code = defines.Codes.CONTENT.number + expected.token = None + expected.payload = None + expected.block2 = (3, 1, 64) + + exchange4 = (req, expected) + self.current_mid += 1 + + req = Request() + req.code = defines.Codes.GET.number + req.proxy_uri = "coap://127.0.0.1:5684/big" + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + req.payload = None + req.block2 = (4, 0, 32) + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = None + expected.code = defines.Codes.CONTENT.number + expected.token = None + expected.payload = None + expected.block2 = (4, 1, 32) + + exchange5 = (req, expected) + self.current_mid += 1 + + req = Request() + req.code = defines.Codes.GET.number + req.proxy_uri = "coap://127.0.0.1:5684/big" + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + req.payload = None + req.block2 = (5, 0, 16) + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = None + expected.code = defines.Codes.CONTENT.number + expected.token = None + expected.payload = None + expected.block2 = (5, 1, 16) + + exchange6 = (req, expected) + self.current_mid += 1 + + req = Request() + req.code = defines.Codes.GET.number + req.proxy_uri = "coap://127.0.0.1:5684/big" + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + req.payload = None + req.block2 = (6, 0, 1024) + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = None + expected.code = defines.Codes.CONTENT.number + expected.token = None + expected.payload = None + expected.block2 = (6, 1, 1024) + + exchange7 = (req, expected) + self.current_mid += 1 + + req = Request() + req.code = defines.Codes.GET.number + req.proxy_uri = "coap://127.0.0.1:5684/big" + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + req.payload = None + req.block2 = (7, 0, 1024) + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = None + expected.code = defines.Codes.CONTENT.number + expected.token = None + expected.payload = None + expected.block2 = (7, 0, 1024) + + exchange8 = (req, expected) + self.current_mid += 1 + + self._test_plugtest([exchange1, exchange2, exchange3, exchange4, exchange5, exchange6, exchange7, exchange8]) + + #self._test_plugtest([exchange1]) + + def test_post_block_big(self): + print("TEST_POST_BLOCK_BIG") + req = Request() + + req.code = defines.Codes.POST.number + req.proxy_uri = "coap://127.0.0.1:5684/big" + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + req.payload = "Lorem ipsum dolo" + req.block1 = (0, 1, 16) + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = None + expected.code = defines.Codes.CONTINUE.number + expected.token = None + expected.payload = None + expected.block1 = (0, 1, 16) + + exchange1 = (req, expected) + self.current_mid += 1 + + req = Request() + req.code = defines.Codes.POST.number + req.proxy_uri = "coap://127.0.0.1:5684/big" + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + req.payload = "r sit amet, consectetur adipisci" + req.block1 = (1, 1, 32) + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = None + expected.code = defines.Codes.CONTINUE.number + expected.token = None + expected.payload = None + expected.block1 = (1, 1, 32) + + exchange2 = (req, expected) + self.current_mid += 1 + + req = Request() + req.code = defines.Codes.POST.number + req.proxy_uri = "coap://127.0.0.1:5684/big" + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + req.payload = "ng elit. Sed ut ultrices ligula. Pellentesque purus augue, cursu" + req.block1 = (2, 1, 64) + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = None + expected.code = defines.Codes.CONTINUE.number + expected.token = None + expected.payload = None + expected.block1 = (2, 1, 64) + + exchange3 = (req, expected) + self.current_mid += 1 + + req = Request() + req.code = defines.Codes.POST.number + req.proxy_uri = "coap://127.0.0.1:5684/big" + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + req.payload = "s ultricies est in, vehicula congue metus. Vestibulum vel justo lacinia, porttitor quam vitae, " \ + "feugiat sapien. Quisque finibus, " + req.block1 = (3, 1, 128) + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = None + expected.code = defines.Codes.CONTINUE.number + expected.token = None + expected.payload = None + expected.block1 = (3, 1, 128) + + exchange4 = (req, expected) + self.current_mid += 1 + + req = Request() + req.code = defines.Codes.POST.number + req.proxy_uri = "coap://127.0.0.1:5684/big" + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + req.payload = "nisi vitae rhoncus malesuada, augue mauris dapibus tellus, sit amet venenatis libero" \ + " libero sed lorem. In pharetra turpis sed eros porta mollis. Quisque dictum dolor nisl," \ + " imperdiet tincidunt augue malesuada vitae. Donec non felis urna. Suspendisse at hend" + req.block1 = (4, 1, 256) + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = None + expected.code = defines.Codes.CONTINUE.number + expected.token = None + expected.payload = None + expected.block1 = (4, 1, 256) + + exchange5 = (req, expected) + self.current_mid += 1 + + req = Request() + req.code = defines.Codes.POST.number + req.proxy_uri = "coap://127.0.0.1:5684/big" + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + req.payload = "rerit ex, quis aliquet ante. Vivamus ultrices dolor at elit tincidunt, eget fringilla " \ + "ligula vestibulum. In molestie sagittis nibh, ut efficitur tellus faucibus non. Maecenas " \ + "posuere elementum faucibus. Morbi nisi diam, molestie non feugiat et, elementum eget magna." \ + " Donec vel sem facilisis quam viverra ultrices nec eu lacus. Sed molestie nisi id ultrices " \ + "interdum. Curabitur pharetra sed tellus in dignissim. Duis placerat aliquam metus, volutpat " \ + "elementum augue aliquam a. Nunc sed dolor at orci maximus portt" + req.block1 = (5, 1, 512) + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = None + expected.code = defines.Codes.CONTINUE.number + expected.token = None + expected.payload = None + expected.block1 = (5, 1, 512) + + exchange6 = (req, expected) + self.current_mid += 1 + + req = Request() + req.code = defines.Codes.POST.number + req.proxy_uri = "coap://127.0.0.1:5684/big" + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + req.payload = "itor ac sit amet eros. Mauris et nisi in tortor pharetra rhoncus sit amet hendrerit metus. " \ + "Integer laoreet placerat cursus. Nam a nulla ex. Donec laoreet sagittis libero quis " \ + "imperdiet. Vivamus facilisis turpis nec rhoncus venenatis. Duis pulvinar tellus vel quam " \ + "maximus imperdiet. Mauris eget nibh orci. Duis ut cursus nibh. Nulla sed commodo elit. " \ + "Suspendisse ac eros lacinia, mattis turpis at, porttitor justo. Vivamus molestie " \ + "tincidunt libero. Etiam porttitor lacus odio, at lobortis tortor scelerisque nec. " \ + "Nullam non ante vel nisi ultrices consectetur. Maecenas massa felis, tempor eget " \ + "malesuada eget, pretium eu sapien. Vivamus dapibus ante erat, non faucibus orci sodales " \ + "sit amet. Cras magna felis, sodales eget magna sed, eleifend rutrum ligula. Vivamus interdum " \ + "enim enim, eu facilisis tortor dignissim quis. Ut metus nulla, mattis non lorem et, " \ + "elementum ultrices orci. Quisque eleifend, arcu vitae ullamcorper pulvinar, ipsum ex " \ + "sodales arcu, eget consectetur mauris metus ac tortor. Donec id sem felis. Maur" + req.block1 = (6, 0, 1024) + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = None + expected.code = defines.Codes.CHANGED.number + expected.token = None + expected.payload = None + expected.location_path = "big" + + exchange7 = (req, expected) + self.current_mid += 1 + + self._test_plugtest([exchange1, exchange2, exchange3, exchange4, exchange5, exchange6, exchange7]) + + # def test_options(self): + # print "TEST_OPTIONS" + # path = "/storage/new_res" + # + # req = Request() + # req.code = defines.Codes.POST.number + # req.uri_path = path + # req.type = defines.Types["CON"] + # req._mid = self.current_mid + # req.destination = self.server_address + # option = Option() + # option.number = defines.OptionRegistry.ETAG.number + # option.value = "test" + # req.add_option(option) + # req.del_option(option) + # req.payload = "test" + # + # expected = Response() + # expected.type = defines.Types["ACK"] + # expected._mid = self.current_mid + # expected.code = defines.Codes.CREATED.number + # expected.token = None + # expected.payload = None + # expected.location_path = "storage/new_res" + # + # exchange1 = (req, expected) + # self.current_mid += 1 + # + # req = Request() + # req.code = defines.Codes.POST.number + # req.uri_path = path + # req.type = defines.Types["CON"] + # req._mid = self.current_mid + # req.destination = self.server_address + # option = Option() + # option.number = defines.OptionRegistry.ETAG.number + # option.value = "test" + # req.add_option(option) + # req.del_option_by_name("ETag") + # req.payload = "test" + # + # expected = Response() + # expected.type = defines.Types["ACK"] + # expected._mid = self.current_mid + # expected.code = defines.Codes.CREATED.number + # expected.token = None + # expected.payload = None + # expected.location_path = "storage/new_res" + # + # exchange2 = (req, expected) + # self.current_mid += 1 + # + # req = Request() + # req.code = defines.Codes.POST.number + # req.uri_path = path + # req.type = defines.Types["CON"] + # req._mid = self.current_mid + # req.destination = self.server_address + # option = Option() + # option.number = defines.OptionRegistry.ETAG.number + # option.value = "test" + # req.add_option(option) + # del req.etag + # req.payload = "test" + # + # expected = Response() + # expected.type = defines.Types["ACK"] + # expected._mid = self.current_mid + # expected.code = defines.Codes.CREATED.number + # expected.token = None + # expected.payload = None + # expected.location_path = "storage/new_res" + # + # exchange3 = (req, expected) + # self.current_mid += 1 + # + # self._test_with_client([exchange1, exchange2, exchange3]) + # + # def test_content_type(self): + # print "TEST_CONTENT_TYPE" + # path = "/storage/new_res" + # + # req = Request() + # req.code = defines.Codes.POST.number + # req.uri_path = path + # req.type = defines.Types["CON"] + # req._mid = self.current_mid + # req.destination = self.server_address + # req.payload = "test" + # req.content_type = defines.Content_types["application/xml"] + # + # expected = Response() + # expected.type = defines.Types["ACK"] + # expected._mid = self.current_mid + # expected.code = defines.Codes.CREATED.number + # expected.token = None + # expected.payload = None + # expected.location_path = "storage/new_res" + # + # exchange1 = (req, expected) + # self.current_mid += 1 + # + # req = Request() + # req.code = defines.Codes.GET.number + # req.uri_path = path + # req.type = defines.Types["CON"] + # req._mid = self.current_mid + # req.destination = self.server_address + # + # expected = Response() + # expected.type = defines.Types["ACK"] + # expected._mid = self.current_mid + # expected.code = defines.Codes.CONTENT.number + # expected.token = None + # expected.payload = "Basic Resource" + # + # exchange2 = (req, expected) + # self.current_mid += 1 + # + # req = Request() + # req.code = defines.Codes.PUT.number + # req.uri_path = path + # req.type = defines.Types["CON"] + # req._mid = self.current_mid + # req.destination = self.server_address + # req.payload = "test" + # + # expected = Response() + # expected.type = defines.Types["ACK"] + # expected._mid = self.current_mid + # expected.code = defines.Codes.CHANGED.number + # expected.token = None + # expected.payload = None + # + # exchange3 = (req, expected) + # self.current_mid += 1 + # + # req = Request() + # req.code = defines.Codes.GET.number + # req.uri_path = path + # req.type = defines.Types["CON"] + # req._mid = self.current_mid + # req.destination = self.server_address + # + # expected = Response() + # expected.type = defines.Types["ACK"] + # expected._mid = self.current_mid + # expected.code = defines.Codes.CONTENT.number + # expected.token = None + # expected.payload = "test" + # + # exchange4 = (req, expected) + # self.current_mid += 1 + # + # req = Request() + # req.code = defines.Codes.GET.number + # req.uri_path = path + # req.type = defines.Types["CON"] + # req._mid = self.current_mid + # req.destination = self.server_address + # req.accept = defines.Content_types["application/xml"] + # + # expected = Response() + # expected.type = defines.Types["ACK"] + # expected._mid = self.current_mid + # expected.code = defines.Codes.CONTENT.number + # expected.token = None + # expected.payload = "test" + # + # exchange5 = (req, expected) + # self.current_mid += 1 + # + # req = Request() + # req.code = defines.Codes.GET.number + # req.uri_path = path + # req.type = defines.Types["CON"] + # req._mid = self.current_mid + # req.destination = self.server_address + # req.accept = defines.Content_types["application/json"] + # + # expected = Response() + # expected.type = defines.Types["ACK"] + # expected._mid = self.current_mid + # expected.code = defines.Codes.NOT_ACCEPTABLE.number + # expected.token = None + # expected.payload = None + # expected.content_type = defines.Content_types["application/json"] + # + # exchange6 = (req, expected) + # self.current_mid += 1 + # + # req = Request() + # req.code = defines.Codes.GET.number + # req.uri_path = "/xml" + # req.type = defines.Types["CON"] + # req._mid = self.current_mid + # req.destination = self.server_address + # + # expected = Response() + # expected.type = defines.Types["ACK"] + # expected._mid = self.current_mid + # expected.code = defines.Codes.CONTENT.number + # expected.token = None + # expected.payload = "0" + # expected.content_type = defines.Content_types["application/xml"] + # + # print(expected.pretty_print()) + # + # exchange7 = (req, expected) + # self.current_mid += 1 + # + # self._test_with_client([exchange1, exchange2, exchange3, exchange4, exchange5, exchange6, exchange7]) + # + # def test_ETAG(self): + # print "TEST_ETAG" + # path = "/etag" + # + # req = Request() + # req.code = defines.Codes.GET.number + # req.uri_path = path + # req.type = defines.Types["CON"] + # req._mid = self.current_mid + # req.destination = self.server_address + # + # expected = Response() + # expected.type = defines.Types["ACK"] + # expected._mid = self.current_mid + # expected.code = defines.Codes.CONTENT.number + # expected.token = None + # expected.payload = "ETag resource" + # expected.etag = "0" + # + # exchange1 = (req, expected) + # self.current_mid += 1 + # + # req = Request() + # req.code = defines.Codes.POST.number + # req.uri_path = path + # req.type = defines.Types["CON"] + # req._mid = self.current_mid + # req.destination = self.server_address + # req.payload = "test" + # + # expected = Response() + # expected.type = defines.Types["ACK"] + # expected._mid = self.current_mid + # expected.code = defines.Codes.CREATED.number + # expected.token = None + # expected.payload = None + # expected.location_path = path + # expected.etag = "1" + # + # exchange2 = (req, expected) + # self.current_mid += 1 + # + # req = Request() + # req.code = defines.Codes.GET.number + # req.uri_path = path + # req.type = defines.Types["CON"] + # req._mid = self.current_mid + # req.destination = self.server_address + # req.etag = "1" + # + # expected = Response() + # expected.type = defines.Types["ACK"] + # expected._mid = self.current_mid + # expected.code = defines.Codes.VALID.number + # expected.token = None + # expected.payload = "test" + # expected.etag = "1" + # + # exchange3 = (req, expected) + # self.current_mid += 1 + # + # self._test_with_client([exchange1, exchange2, exchange3]) + # + # def test_child(self): + # print "TEST_CHILD" + # path = "/child" + # + # req = Request() + # req.code = defines.Codes.POST.number + # req.uri_path = path + # req.type = defines.Types["CON"] + # req._mid = self.current_mid + # req.destination = self.server_address + # req.payload = "test" + # + # expected = Response() + # expected.type = defines.Types["ACK"] + # expected._mid = self.current_mid + # expected.code = defines.Codes.CREATED.number + # expected.token = None + # expected.payload = None + # expected.location_path = path + # + # exchange1 = (req, expected) + # self.current_mid += 1 + # + # req = Request() + # req.code = defines.Codes.GET.number + # req.uri_path = path + # req.type = defines.Types["CON"] + # req._mid = self.current_mid + # req.destination = self.server_address + # + # expected = Response() + # expected.type = defines.Types["ACK"] + # expected._mid = self.current_mid + # expected.code = defines.Codes.CONTENT.number + # expected.token = None + # expected.payload = "test" + # + # exchange2 = (req, expected) + # self.current_mid += 1 + # + # req = Request() + # req.code = defines.Codes.PUT.number + # req.uri_path = path + # req.type = defines.Types["CON"] + # req._mid = self.current_mid + # req.destination = self.server_address + # req.payload = "testPUT" + # + # expected = Response() + # expected.type = defines.Types["ACK"] + # expected._mid = self.current_mid + # expected.code = defines.Codes.CHANGED.number + # expected.token = None + # expected.payload = None + # + # exchange3 = (req, expected) + # self.current_mid += 1 + # + # req = Request() + # req.code = defines.Codes.DELETE.number + # req.uri_path = path + # req.type = defines.Types["CON"] + # req._mid = self.current_mid + # req.destination = self.server_address + # + # expected = Response() + # expected.type = defines.Types["ACK"] + # expected._mid = self.current_mid + # expected.code = defines.Codes.DELETED.number + # expected.token = None + # expected.payload = None + # + # exchange4 = (req, expected) + # self.current_mid += 1 + # + # self._test_with_client([exchange1, exchange2, exchange3, exchange4]) + # + # def test_not_found(self): + # print "TEST_not_found" + # path = "/not_found" + # + # req = Request() + # req.code = defines.Codes.GET.number + # req.uri_path = path + # req.type = defines.Types["CON"] + # req._mid = self.current_mid + # req.destination = self.server_address + # req.token = 100 + # + # expected = Response() + # expected.type = defines.Types["ACK"] + # expected._mid = self.current_mid + # expected.code = defines.Codes.NOT_FOUND.number + # expected.token = "100" + # expected.payload = None + # + # exchange1 = (req, expected) + # self.current_mid += 1 + # + # req = Request() + # req.code = defines.Codes.POST.number + # req.uri_path = path + # req.type = defines.Types["CON"] + # req._mid = self.current_mid + # req.destination = self.server_address + # req.payload = "test" + # + # expected = Response() + # expected.type = defines.Types["ACK"] + # expected._mid = self.current_mid + # expected.code = defines.Codes.METHOD_NOT_ALLOWED.number + # expected.token = None + # + # exchange2 = (req, expected) + # self.current_mid += 1 + # + # req = Request() + # req.code = defines.Codes.PUT.number + # req.uri_path = path + # req.type = defines.Types["CON"] + # req._mid = self.current_mid + # req.destination = self.server_address + # req.payload = "testPUT" + # + # expected = Response() + # expected.type = defines.Types["ACK"] + # expected._mid = self.current_mid + # expected.code = defines.Codes.NOT_FOUND.number + # expected.token = None + # expected.payload = None + # + # exchange3 = (req, expected) + # self.current_mid += 1 + # + # req = Request() + # req.code = defines.Codes.DELETE.number + # req.uri_path = path + # req.type = defines.Types["CON"] + # req._mid = self.current_mid + # req.destination = self.server_address + # + # expected = Response() + # expected.type = defines.Types["ACK"] + # expected._mid = self.current_mid + # expected.code = defines.Codes.NOT_FOUND.number + # expected.token = None + # expected.payload = None + # + # exchange4 = (req, expected) + # self.current_mid += 1 + # + # self._test_with_client([exchange1, exchange2, exchange3, exchange4]) + # + # def test_invalid(self): + # print("TEST_INVALID") + # + # # version + # req = (bytes("\x00\x01\x8c\xda", "utf-8"), self.server_address) + # + # expected = Response() + # expected.type = defines.Types["RST"] + # expected._mid = None + # expected.code = defines.Codes.BAD_REQUEST.number + # + # exchange1 = (req, expected) + # + # # version + # req = (bytes("\x40", "utf-8"), self.server_address) + # + # expected = Response() + # expected.type = defines.Types["RST"] + # expected._mid = None + # expected.code = defines.Codes.BAD_REQUEST.number + # + # exchange2 = (req, expected) + # + # # code + # req = (bytes("\x40\x05\x8c\xda", "utf-8"), self.server_address) + # + # expected = Response() + # expected.type = defines.Types["RST"] + # expected._mid = None + # expected.code = defines.Codes.BAD_REQUEST.number + # + # exchange3 = (req, expected) + # + # # option + # req = (bytes("\x40\x01\x8c\xda\x94", "utf-8"), self.server_address) + # + # expected = Response() + # expected.type = defines.Types["RST"] + # expected._mid = None + # expected.code = defines.Codes.BAD_REQUEST.number + # + # exchange4 = (req, expected) + # + # # payload marker + # req = (bytes("\x40\x02\x8c\xda\x75\x62\x61\x73\x69\x63\xff", "utf-8"), self.server_address) + # + # expected = Response() + # expected.type = defines.Types["RST"] + # expected._mid = None + # expected.code = defines.Codes.BAD_REQUEST.number + # + # exchange5 = (req, expected) + # + # self._test_datagram([exchange1, exchange2, exchange3, exchange4, exchange5]) + + def test_post_block_big_client(self): + print("TEST_POST_BLOCK_BIG_CLIENT") + req = Request() + + req.code = defines.Codes.POST.number + req.proxy_uri = "coap://127.0.0.1:5684/big" + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + req.payload = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras sollicitudin fermentum ornare. " \ + "Cras accumsan tellus quis dui lacinia eleifend. Proin ultrices rutrum orci vitae luctus. " \ + "Nullam malesuada pretium elit, at aliquam odio vehicula in. Etiam nec maximus elit. " \ + "Etiam at erat ac ex ornare feugiat. Curabitur sed malesuada orci, id aliquet nunc. Phasellus " \ + "nec leo luctus, blandit lorem sit amet, interdum metus. Duis efficitur volutpat magna, ac " \ + "ultricies nibh aliquet sit amet. Etiam tempor egestas augue in hendrerit. Nunc eget augue " \ + "ultricies, dignissim lacus et, vulputate dolor. Nulla eros odio, fringilla vel massa ut, " \ + "facilisis cursus quam. Fusce faucibus lobortis congue. Fusce consectetur porta neque, id " \ + "sollicitudin velit maximus eu. Sed pharetra leo quam, vel finibus turpis cursus ac. " \ + "Aenean ac nisi massa. Cras commodo arcu nec ante tristique ullamcorper. Quisque eu hendrerit" \ + " urna. Cras fringilla eros ut nunc maximus, non porta nisl mollis. Aliquam in rutrum massa." \ + " Praesent tristique turpis dui, at ultricies lorem fermentum at. Vivamus sit amet ornare neque, " \ + "a imperdiet nisl. Quisque a iaculis libero, id tempus lacus. Aenean convallis est non justo " \ + "consectetur, a hendrerit enim consequat. In accumsan ante a egestas luctus. Etiam quis neque " \ + "nec eros vestibulum faucibus. Nunc viverra ipsum lectus, vel scelerisque dui dictum a. Ut orci " \ + "enim, ultrices a ultrices nec, pharetra in quam. Donec accumsan sit amet eros eget fermentum." \ + "Vivamus ut odio ac odio malesuada accumsan. Aenean vehicula diam at tempus ornare. Phasellus " \ + "dictum mauris a mi consequat, vitae mattis nulla fringilla. Ut laoreet tellus in nisl efficitur," \ + " a luctus justo tempus. Fusce finibus libero eget velit finibus iaculis. Morbi rhoncus purus " \ + "vel vestibulum ullamcorper. Sed ac metus in urna fermentum feugiat. Nulla nunc diam, sodales " \ + "aliquam mi id, varius porta nisl. Praesent vel nibh ac turpis rutrum laoreet at non odio. " \ + "Phasellus ut posuere mi. Suspendisse malesuada velit nec mauris convallis porta. Vivamus " \ + "sed ultrices sapien, at cras amet." + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = None + expected.code = defines.Codes.CHANGED.number + expected.token = None + expected.payload = None + + exchange1 = (req, expected) + self.current_mid += 1 + + self._test_with_client([exchange1]) + + def test_observe_client(self): + print("TEST_OBSERVE_CLIENT") + + req = Request() + req.code = defines.Codes.GET.number + req.proxy_uri = "coap://127.0.0.1:5684/basic" + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + req.observe = 0 + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = None + expected.code = defines.Codes.CONTENT.number + expected.token = None + expected.payload = None + + exchange1 = (req, expected) + + self.current_mid += 1 + + self._test_with_client_observe([exchange1]) + + def test_duplicate(self): + print("TEST_DUPLICATE") + req = Request() + req.code = defines.Codes.GET.number + req.proxy_uri = "coap://127.0.0.1:5684/basic" + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = self.current_mid + expected.code = defines.Codes.CONTENT.number + expected.token = None + + self.current_mid += 1 + self._test_plugtest([(req, expected), (req, expected)]) + + def test_duplicate_not_completed(self): + print("TEST_DUPLICATE_NOT_COMPLETED") + req = Request() + req.code = defines.Codes.GET.number + req.proxy_uri = "coap://127.0.0.1:5684/long" + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = self.current_mid + expected.code = None + expected.token = None + + expected2 = Response() + expected2.type = defines.Types["CON"] + expected2._mid = None + expected2.code = defines.Codes.CONTENT.number + expected2.token = None + + self.current_mid += 1 + self._test_plugtest([(req, None), (req, expected), (None, expected2)]) + +if __name__ == '__main__': + unittest.main() + diff --git a/coverage_test_reverse_proxy.py b/coverage_test_reverse_proxy.py new file mode 100644 index 0000000..beab967 --- /dev/null +++ b/coverage_test_reverse_proxy.py @@ -0,0 +1,1473 @@ +from queue import Queue +import random +import socket +import threading +import unittest +from coapclient import HelperClient +from coapforwardproxy import CoAPForwardProxy +from coapreverseproxy import CoAPReverseProxy +from coapserver import CoAPServer +from coapthon import defines +from coapthon.messages.message import Message +from coapthon.messages.option import Option +from coapthon.messages.request import Request +from coapthon.messages.response import Response +from coapthon.serializer import Serializer + +__author__ = 'Giacomo Tanganelli' +__version__ = "2.0" + + +class Tests(unittest.TestCase): + + def setUp(self): + self.server_address = ("127.0.0.1", 5683) + self.current_mid = random.randint(1, 1000) + self.server_mid = random.randint(1000, 2000) + self.server = CoAPServer("127.0.0.1", 5684) + self.server_thread = threading.Thread(target=self.server.listen, args=(10,)) + self.server_thread.start() + self.proxy = CoAPReverseProxy("127.0.0.1", 5683, "reverse_proxy_mapping.xml") + self.proxy_thread = threading.Thread(target=self.proxy.listen, args=(10,)) + self.proxy_thread.start() + self.queue = Queue() + + def tearDown(self): + self.server.close() + self.server_thread.join(timeout=25) + self.server = None + self.proxy.close() + self.proxy_thread.join(timeout=25) + self.proxy = None + + def _test_with_client(self, message_list): # pragma: no cover + client = HelperClient(self.server_address) + for message, expected in message_list: + if message is not None: + received_message = client.send_request(message) + if expected is not None: + if expected.type is not None: + self.assertEqual(received_message.type, expected.type) + if expected.mid is not None: + self.assertEqual(received_message.mid, expected.mid) + self.assertEqual(received_message.code, expected.code) + if expected.source is not None: + self.assertEqual(received_message.source, self.server_address) + if expected.token is not None: + self.assertEqual(received_message.token, expected.token) + if expected.payload is not None: + self.assertEqual(received_message.payload, expected.payload) + if expected.options: + self.assertEqual(len(received_message.options), len(expected.options)) + for o in expected.options: + assert isinstance(o, Option) + option_value = getattr(expected, o.name.lower().replace("-", "_")) + option_value_rec = getattr(received_message, o.name.lower().replace("-", "_")) + self.assertEqual(option_value, option_value_rec) + client.stop() + + def _test_with_client_observe(self, message_list): # pragma: no cover + client = HelperClient(self.server_address) + for message, expected in message_list: + if message is not None: + client.send_request(message, self.client_callback) + if expected is not None: + received_message = self.queue.get() + if expected.type is not None: + self.assertEqual(received_message.type, expected.type) + if expected.mid is not None: + self.assertEqual(received_message.mid, expected.mid) + self.assertEqual(received_message.code, expected.code) + if expected.source is not None: + self.assertEqual(received_message.source, self.server_address) + if expected.token is not None: + self.assertEqual(received_message.token, expected.token) + if expected.payload is not None: + self.assertEqual(received_message.payload, expected.payload) + if expected.options: + self.assertEqual(len(received_message.options), len(expected.options)) + for o in expected.options: + assert isinstance(o, Option) + option_value = getattr(expected, o.name.lower().replace("-", "_")) + option_value_rec = getattr(received_message, o.name.lower().replace("-", "_")) + self.assertEqual(option_value, option_value_rec) + client.stop() + + def client_callback(self, response): # pragma: no cover + print("Callback") + self.queue.put(response) + + def _test_plugtest(self, message_list): # pragma: no cover + serializer = Serializer() + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + for message, expected in message_list: + if message is not None: + datagram = serializer.serialize(message) + sock.sendto(datagram, message.destination) + if expected is not None: + datagram, source = sock.recvfrom(4096) + received_message = serializer.deserialize(datagram, source) + if expected.type is not None: + self.assertEqual(received_message.type, expected.type) + if expected.mid is not None: + self.assertEqual(received_message.mid, expected.mid) + self.assertEqual(received_message.code, expected.code) + if expected.source is not None: + self.assertEqual(received_message.source, source) + if expected.token is not None: + self.assertEqual(received_message.token, expected.token) + if expected.payload is not None: + self.assertEqual(received_message.payload, expected.payload) + if expected.options is not None: + self.assertEqual(received_message.options, expected.options) + for o in expected.options: + assert isinstance(o, Option) + option_value = getattr(expected, o.name.lower().replace("-", "_")) + option_value_rec = getattr(received_message, o.name.lower().replace("-", "_")) + self.assertEqual(option_value, option_value_rec) + sock.close() + + def _test_datagram(self, message_list): # pragma: no cover + serializer = Serializer() + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + for message, expected in message_list: + if message is not None: + datagram, destination = message + sock.sendto(datagram, destination) + if expected is not None: + datagram, source = sock.recvfrom(4096) + received_message = serializer.deserialize(datagram, source) + if expected.type is not None: + self.assertEqual(received_message.type, expected.type) + if expected.mid is not None: + self.assertEqual(received_message.mid, expected.mid) + self.assertEqual(received_message.code, expected.code) + if expected.source is not None: + self.assertEqual(received_message.source, source) + if expected.token is not None: + self.assertEqual(received_message.token, expected.token) + if expected.payload is not None: + self.assertEqual(received_message.payload, expected.payload) + if expected.options is not None: + self.assertEqual(received_message.options, expected.options) + for o in expected.options: + assert isinstance(o, Option) + option_value = getattr(expected, o.name.lower().replace("-", "_")) + option_value_rec = getattr(received_message, o.name.lower().replace("-", "_")) + self.assertEqual(option_value, option_value_rec) + sock.close() + + def test_get_reverse(self): + print("TEST_GET_REVERSE") + path = "/Server1/basic" + req = Request() + req.code = defines.Codes.GET.number + req.uri_path = path + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = self.current_mid + expected.code = defines.Codes.CONTENT.number + expected.token = None + expected.payload = "Basic Resource" + + exchange1 = (req, expected) + + self.current_mid += 1 + + req = Request() + req.code = defines.Codes.POST.number + req.uri_path = path + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = self.current_mid + expected.code = defines.Codes.CREATED.number + expected.token = None + + exchange2 = (req, expected) + + self.current_mid += 1 + + req = Request() + req.code = defines.Codes.PUT.number + req.uri_path = path + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = self.current_mid + expected.code = defines.Codes.CHANGED.number + expected.token = None + + exchange3 = (req, expected) + + self.current_mid += 1 + + req = Request() + req.code = defines.Codes.DELETE.number + req.uri_path = path + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = self.current_mid + expected.code = defines.Codes.DELETED.number + expected.token = None + + exchange4 = (req, expected) + + self.current_mid += 1 + self._test_with_client([exchange1, exchange2, exchange3, exchange4]) + + def test_separate(self): + print("TEST_SEPARATE") + path = "/Server1/separate" + req = Request() + req.code = defines.Codes.GET.number + req.uri_path = path + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + + expected = Response() + expected.type = defines.Types["CON"] + expected._mid = None + expected.code = defines.Codes.CONTENT.number + expected.token = None + expected.max_age = 60 + + exchange1 = (req, expected) + + self.current_mid += 1 + + req = Request() + req.code = defines.Codes.POST.number + req.uri_path = path + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + req.payload = "POST" + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = None + expected.code = defines.Codes.CHANGED.number + expected.token = None + expected.options = None + + exchange2 = (req, expected) + + self.current_mid += 1 + + req = Request() + req.code = defines.Codes.PUT.number + req.uri_path = path + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + req.payload = "PUT" + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = None + expected.code = defines.Codes.CHANGED.number + expected.token = None + expected.options = None + + exchange3 = (req, expected) + + self.current_mid += 1 + + req = Request() + req.code = defines.Codes.DELETE.number + req.uri_path = path + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = None + expected.code = defines.Codes.DELETED.number + expected.token = None + + exchange4 = (req, expected) + + self.current_mid += 1 + self._test_with_client([exchange1, exchange2, exchange3, exchange4]) + + def test_post(self): + print("TEST_POST") + path = "/Server1/storage/new_res?id=1" + req = Request() + + req.code = defines.Codes.POST.number + req.uri_path = path + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + req.payload = "test" + req.add_if_none_match() + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = self.current_mid + expected.code = defines.Codes.CREATED.number + expected.token = None + expected.payload = None + expected.location_path = "Server1/storage/new_res" + expected.location_query = "id=1" + + exchange1 = (req, expected) + self.current_mid += 1 + + req = Request() + req.code = defines.Codes.GET.number + req.uri_path = "/Server1/storage/new_res" + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + req.if_match = ["test", "not"] + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = self.current_mid + expected.code = defines.Codes.CONTENT.number + expected.token = None + expected.payload = "test" + + exchange2 = (req, expected) + self.current_mid += 1 + + req = Request() + req.code = defines.Codes.PUT.number + req.uri_path = "/Server1/storage/new_res" + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + req.if_match = ["not"] + req.payload = "not" + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = self.current_mid + expected.code = defines.Codes.PRECONDITION_FAILED.number + expected.token = None + + exchange3 = (req, expected) + self.current_mid += 1 + + req = Request() + req.code = defines.Codes.POST.number + req.uri_path = "/Server1/storage/new_res" + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + req.if_match = ["not"] + req.payload = "not" + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = self.current_mid + expected.code = defines.Codes.PRECONDITION_FAILED.number + expected.token = None + + exchange4 = (req, expected) + self.current_mid += 1 + + req = Request() + req.code = defines.Codes.PUT.number + req.uri_path = "/Server1/storage/new_res" + req._mid = self.current_mid + req.destination = self.server_address + req.add_if_none_match() + req.payload = "not" + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = self.current_mid + expected.code = defines.Codes.PRECONDITION_FAILED.number + expected.token = None + + exchange5 = (req, expected) + self.current_mid += 1 + + self._test_with_client([exchange1, exchange2, exchange3, exchange4, exchange5]) + + def test_post_block(self): + print("TEST_POST_BLOCK") + path = "/Server1/storage/new_res" + req = Request() + + req.code = defines.Codes.POST.number + req.uri_path = path + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + req.payload = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras sollicitudin fermentum ornare. " \ + "Cras accumsan tellus quis dui lacinia eleifend. Proin ultrices rutrum orci vitae luctus. " \ + "Nullam malesuada pretium elit, at aliquam odio vehicula in. Etiam nec maximus elit. " \ + "Etiam at erat ac ex ornare feugiat. Curabitur sed malesuada orci, id aliquet nunc. Phasellus " \ + "nec leo luctus, blandit lorem sit amet, interdum metus. Duis efficitur volutpat magna, ac " \ + "ultricies nibh aliquet sit amet. Etiam tempor egestas augue in hendrerit. Nunc eget augue " \ + "ultricies, dignissim lacus et, vulputate dolor. Nulla eros odio, fringilla vel massa ut, " \ + "facilisis cursus quam. Fusce faucibus lobortis congue. Fusce consectetur porta neque, id " \ + "sollicitudin velit maximus eu. Sed pharetra leo quam, vel finibus turpis cursus ac. " \ + "Aenean ac nisi massa. Cras commodo arcu nec ante tristique ullamcorper. Quisque eu hendrerit" \ + " urna. Cras fringilla eros ut nunc maximus, non porta nisl mollis. Aliquam in rutrum massa." \ + " Praesent tristique turpis dui, at ultri" + req.block1 = (1, 1, 1024) + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = None + expected.code = defines.Codes.REQUEST_ENTITY_INCOMPLETE.number + expected.token = None + expected.payload = None + + exchange1 = (req, expected) + self.current_mid += 1 + + req = Request() + req.code = defines.Codes.POST.number + req.uri_path = path + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + req.payload = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras sollicitudin fermentum ornare. " \ + "Cras accumsan tellus quis dui lacinia eleifend. Proin ultrices rutrum orci vitae luctus. " \ + "Nullam malesuada pretium elit, at aliquam odio vehicula in. Etiam nec maximus elit. " \ + "Etiam at erat ac ex ornare feugiat. Curabitur sed malesuada orci, id aliquet nunc. Phasellus " \ + "nec leo luctus, blandit lorem sit amet, interdum metus. Duis efficitur volutpat magna, ac " \ + "ultricies nibh aliquet sit amet. Etiam tempor egestas augue in hendrerit. Nunc eget augue " \ + "ultricies, dignissim lacus et, vulputate dolor. Nulla eros odio, fringilla vel massa ut, " \ + "facilisis cursus quam. Fusce faucibus lobortis congue. Fusce consectetur porta neque, id " \ + "sollicitudin velit maximus eu. Sed pharetra leo quam, vel finibus turpis cursus ac. " \ + "Aenean ac nisi massa. Cras commodo arcu nec ante tristique ullamcorper. Quisque eu hendrerit" \ + " urna. Cras fringilla eros ut nunc maximus, non porta nisl mollis. Aliquam in rutrum massa." \ + " Praesent tristique turpis dui, at ultri" + req.block1 = (0, 1, 1024) + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = None + expected.code = defines.Codes.CONTINUE.number + expected.token = None + expected.payload = None + expected.block1 = (0, 1, 1024) + + exchange2 = (req, expected) + self.current_mid += 1 + + req = Request() + req.code = defines.Codes.POST.number + req.uri_path = path + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + req.payload = "a imperdiet nisl. Quisque a iaculis libero, id tempus lacus. Aenean convallis est non justo " \ + "consectetur, a hendrerit enim consequat. In accumsan ante a egestas luctus. Etiam quis neque " \ + "nec eros vestibulum faucibus. Nunc viverra ipsum lectus, vel scelerisque dui dictum a. Ut orci " \ + "enim, ultrices a ultrices nec, pharetra in quam. Donec accumsan sit amet eros eget fermentum." + req.block1 = (1, 1, 64) + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = None + expected.code = defines.Codes.CONTINUE.number + expected.token = None + expected.payload = None + expected.block1 = (1, 1, 64) + + exchange3 = (req, expected) + self.current_mid += 1 + + req = Request() + req.code = defines.Codes.POST.number + req.uri_path = path + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + req.payload = "a imperdiet nisl. Quisque a iaculis libero, id tempus lacus. Aenean convallis est non justo " \ + "consectetur, a hendrerit enim consequat. In accumsan ante a egestas luctus. Etiam quis neque " \ + "nec eros vestibulum faucibus. Nunc viverra ipsum lectus, vel scelerisque dui dictum a. Ut orci " \ + "enim, ultrices a ultrices nec, pharetra in quam. Donec accumsan sit amet eros eget fermentum." + req.block1 = (3, 1, 64) + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = None + expected.code = defines.Codes.REQUEST_ENTITY_INCOMPLETE.number + expected.token = None + expected.payload = None + + exchange4 = (req, expected) + self.current_mid += 1 + + req = Request() + req.code = defines.Codes.POST.number + req.uri_path = path + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + req.payload = "a imperdiet nisl. Quisque a iaculis libero, id tempus lacus. Aenean convallis est non justo " \ + "consectetur, a hendrerit enim consequat. In accumsan ante a egestas luctus. Etiam quis neque " \ + "nec eros vestibulum faucibus. Nunc viverra ipsum lectus, vel scelerisque dui dictum a. Ut orci " \ + "enim, ultrices a ultrices nec, pharetra in quam. Donec accumsan sit amet eros eget fermentum." + req.block1 = (2, 0, 64) + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = None + expected.code = defines.Codes.CREATED.number + expected.token = None + expected.payload = None + expected.location_path = "/Server1/storage/new_res" + + exchange5 = (req, expected) + self.current_mid += 1 + + self._test_plugtest([exchange1, exchange2, exchange3, exchange4, exchange5]) + + def test_get_block(self): + print("TEST_GET_BLOCK") + path = "/Server1/big" + + req = Request() + req.code = defines.Codes.GET.number + req.uri_path = path + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + req.payload = None + req.block2 = (0, 0, 512) + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = None + expected.code = defines.Codes.CONTENT.number + expected.token = None + expected.payload = None + expected.block2 = (0, 1, 512) + + exchange1 = (req, expected) + self.current_mid += 1 + + req = Request() + req.code = defines.Codes.GET.number + req.uri_path = path + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + req.payload = None + req.block2 = (1, 0, 256) + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = None + expected.code = defines.Codes.CONTENT.number + expected.token = None + expected.payload = None + expected.block2 = (1, 1, 256) + + exchange2 = (req, expected) + self.current_mid += 1 + + req = Request() + req.code = defines.Codes.GET.number + req.uri_path = path + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + req.payload = None + req.block2 = (2, 0, 128) + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = None + expected.code = defines.Codes.CONTENT.number + expected.token = None + expected.payload = None + expected.block2 = (2, 1, 128) + + exchange3 = (req, expected) + self.current_mid += 1 + + req = Request() + req.code = defines.Codes.GET.number + req.uri_path = path + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + req.payload = None + req.block2 = (3, 0, 64) + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = None + expected.code = defines.Codes.CONTENT.number + expected.token = None + expected.payload = None + expected.block2 = (3, 1, 64) + + exchange4 = (req, expected) + self.current_mid += 1 + + req = Request() + req.code = defines.Codes.GET.number + req.uri_path = path + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + req.payload = None + req.block2 = (4, 0, 32) + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = None + expected.code = defines.Codes.CONTENT.number + expected.token = None + expected.payload = None + expected.block2 = (4, 1, 32) + + exchange5 = (req, expected) + self.current_mid += 1 + + req = Request() + req.code = defines.Codes.GET.number + req.uri_path = path + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + req.payload = None + req.block2 = (5, 0, 16) + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = None + expected.code = defines.Codes.CONTENT.number + expected.token = None + expected.payload = None + expected.block2 = (5, 1, 16) + + exchange6 = (req, expected) + self.current_mid += 1 + + req = Request() + req.code = defines.Codes.GET.number + req.uri_path = path + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + req.payload = None + req.block2 = (6, 0, 1024) + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = None + expected.code = defines.Codes.CONTENT.number + expected.token = None + expected.payload = None + expected.block2 = (6, 1, 1024) + + exchange7 = (req, expected) + self.current_mid += 1 + + req = Request() + req.code = defines.Codes.GET.number + req.uri_path = path + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + req.payload = None + req.block2 = (7, 0, 1024) + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = None + expected.code = defines.Codes.CONTENT.number + expected.token = None + expected.payload = None + expected.block2 = (7, 0, 1024) + + exchange8 = (req, expected) + self.current_mid += 1 + + self._test_plugtest([exchange1, exchange2, exchange3, exchange4, exchange5, exchange6, exchange7, exchange8]) + + # def test_post_block_big(self): + # print "TEST_POST_BLOCK_BIG" + # path = "/big" + # req = Request() + # + # req.code = defines.Codes.POST.number + # req.uri_path = path + # req.type = defines.Types["CON"] + # req._mid = self.current_mid + # req.destination = self.server_address + # req.payload = "Lorem ipsum dolo" + # req.block1 = (0, 1, 16) + # + # expected = Response() + # expected.type = defines.Types["ACK"] + # expected._mid = None + # expected.code = defines.Codes.CONTINUE.number + # expected.token = None + # expected.payload = None + # expected.block1 = (0, 1, 16) + # + # exchange1 = (req, expected) + # self.current_mid += 1 + # + # req = Request() + # req.code = defines.Codes.POST.number + # req.uri_path = path + # req.type = defines.Types["CON"] + # req._mid = self.current_mid + # req.destination = self.server_address + # req.payload = "r sit amet, consectetur adipisci" + # req.block1 = (1, 1, 32) + # + # expected = Response() + # expected.type = defines.Types["ACK"] + # expected._mid = None + # expected.code = defines.Codes.CONTINUE.number + # expected.token = None + # expected.payload = None + # expected.block1 = (1, 1, 32) + # + # exchange2 = (req, expected) + # self.current_mid += 1 + # + # req = Request() + # req.code = defines.Codes.POST.number + # req.uri_path = path + # req.type = defines.Types["CON"] + # req._mid = self.current_mid + # req.destination = self.server_address + # req.payload = "ng elit. Sed ut ultrices ligula. Pellentesque purus augue, cursu" + # req.block1 = (2, 1, 64) + # + # expected = Response() + # expected.type = defines.Types["ACK"] + # expected._mid = None + # expected.code = defines.Codes.CONTINUE.number + # expected.token = None + # expected.payload = None + # expected.block1 = (2, 1, 64) + # + # exchange3 = (req, expected) + # self.current_mid += 1 + # + # req = Request() + # req.code = defines.Codes.POST.number + # req.uri_path = path + # req.type = defines.Types["CON"] + # req._mid = self.current_mid + # req.destination = self.server_address + # req.payload = "s ultricies est in, vehicula congue metus. Vestibulum vel justo lacinia, porttitor quam vitae, " \ + # "feugiat sapien. Quisque finibus, " + # req.block1 = (3, 1, 128) + # + # expected = Response() + # expected.type = defines.Types["ACK"] + # expected._mid = None + # expected.code = defines.Codes.CONTINUE.number + # expected.token = None + # expected.payload = None + # expected.block1 = (3, 1, 128) + # + # exchange4 = (req, expected) + # self.current_mid += 1 + # + # req = Request() + # req.code = defines.Codes.POST.number + # req.uri_path = path + # req.type = defines.Types["CON"] + # req._mid = self.current_mid + # req.destination = self.server_address + # req.payload = "nisi vitae rhoncus malesuada, augue mauris dapibus tellus, sit amet venenatis libero" \ + # " libero sed lorem. In pharetra turpis sed eros porta mollis. Quisque dictum dolor nisl," \ + # " imperdiet tincidunt augue malesuada vitae. Donec non felis urna. Suspendisse at hend" + # req.block1 = (4, 1, 256) + # + # expected = Response() + # expected.type = defines.Types["ACK"] + # expected._mid = None + # expected.code = defines.Codes.CONTINUE.number + # expected.token = None + # expected.payload = None + # expected.block1 = (4, 1, 256) + # + # exchange5 = (req, expected) + # self.current_mid += 1 + # + # req = Request() + # req.code = defines.Codes.POST.number + # req.uri_path = path + # req.type = defines.Types["CON"] + # req._mid = self.current_mid + # req.destination = self.server_address + # req.payload = "rerit ex, quis aliquet ante. Vivamus ultrices dolor at elit tincidunt, eget fringilla " \ + # "ligula vestibulum. In molestie sagittis nibh, ut efficitur tellus faucibus non. Maecenas " \ + # "posuere elementum faucibus. Morbi nisi diam, molestie non feugiat et, elementum eget magna." \ + # " Donec vel sem facilisis quam viverra ultrices nec eu lacus. Sed molestie nisi id ultrices " \ + # "interdum. Curabitur pharetra sed tellus in dignissim. Duis placerat aliquam metus, volutpat " \ + # "elementum augue aliquam a. Nunc sed dolor at orci maximus portt" + # req.block1 = (5, 1, 512) + # + # expected = Response() + # expected.type = defines.Types["ACK"] + # expected._mid = None + # expected.code = defines.Codes.CONTINUE.number + # expected.token = None + # expected.payload = None + # expected.block1 = (5, 1, 512) + # + # exchange6 = (req, expected) + # self.current_mid += 1 + # + # req = Request() + # req.code = defines.Codes.POST.number + # req.uri_path = path + # req.type = defines.Types["CON"] + # req._mid = self.current_mid + # req.destination = self.server_address + # req.payload = "itor ac sit amet eros. Mauris et nisi in tortor pharetra rhoncus sit amet hendrerit metus. " \ + # "Integer laoreet placerat cursus. Nam a nulla ex. Donec laoreet sagittis libero quis " \ + # "imperdiet. Vivamus facilisis turpis nec rhoncus venenatis. Duis pulvinar tellus vel quam " \ + # "maximus imperdiet. Mauris eget nibh orci. Duis ut cursus nibh. Nulla sed commodo elit. " \ + # "Suspendisse ac eros lacinia, mattis turpis at, porttitor justo. Vivamus molestie " \ + # "tincidunt libero. Etiam porttitor lacus odio, at lobortis tortor scelerisque nec. " \ + # "Nullam non ante vel nisi ultrices consectetur. Maecenas massa felis, tempor eget " \ + # "malesuada eget, pretium eu sapien. Vivamus dapibus ante erat, non faucibus orci sodales " \ + # "sit amet. Cras magna felis, sodales eget magna sed, eleifend rutrum ligula. Vivamus interdum " \ + # "enim enim, eu facilisis tortor dignissim quis. Ut metus nulla, mattis non lorem et, " \ + # "elementum ultrices orci. Quisque eleifend, arcu vitae ullamcorper pulvinar, ipsum ex " \ + # "sodales arcu, eget consectetur mauris metus ac tortor. Donec id sem felis. Maur" + # req.block1 = (6, 0, 1024) + # + # expected = Response() + # expected.type = defines.Types["ACK"] + # expected._mid = None + # expected.code = defines.Codes.CREATED.number + # expected.token = None + # expected.payload = None + # expected.location_path = "big" + # + # exchange7 = (req, expected) + # self.current_mid += 1 + # + # self._test_plugtest([exchange1, exchange2, exchange3, exchange4, exchange5, exchange6, exchange7]) + # + # def test_options(self): + # print "TEST_OPTIONS" + # path = "/storage/new_res" + # + # req = Request() + # req.code = defines.Codes.POST.number + # req.uri_path = path + # req.type = defines.Types["CON"] + # req._mid = self.current_mid + # req.destination = self.server_address + # option = Option() + # option.number = defines.OptionRegistry.ETAG.number + # option.value = "test" + # req.add_option(option) + # req.del_option(option) + # req.payload = "test" + # + # expected = Response() + # expected.type = defines.Types["ACK"] + # expected._mid = self.current_mid + # expected.code = defines.Codes.CREATED.number + # expected.token = None + # expected.payload = None + # expected.location_path = "storage/new_res" + # + # exchange1 = (req, expected) + # self.current_mid += 1 + # + # req = Request() + # req.code = defines.Codes.POST.number + # req.uri_path = path + # req.type = defines.Types["CON"] + # req._mid = self.current_mid + # req.destination = self.server_address + # option = Option() + # option.number = defines.OptionRegistry.ETAG.number + # option.value = "test" + # req.add_option(option) + # req.del_option_by_name("ETag") + # req.payload = "test" + # + # expected = Response() + # expected.type = defines.Types["ACK"] + # expected._mid = self.current_mid + # expected.code = defines.Codes.CREATED.number + # expected.token = None + # expected.payload = None + # expected.location_path = "storage/new_res" + # + # exchange2 = (req, expected) + # self.current_mid += 1 + # + # req = Request() + # req.code = defines.Codes.POST.number + # req.uri_path = path + # req.type = defines.Types["CON"] + # req._mid = self.current_mid + # req.destination = self.server_address + # option = Option() + # option.number = defines.OptionRegistry.ETAG.number + # option.value = "test" + # req.add_option(option) + # del req.etag + # req.payload = "test" + # + # expected = Response() + # expected.type = defines.Types["ACK"] + # expected._mid = self.current_mid + # expected.code = defines.Codes.CREATED.number + # expected.token = None + # expected.payload = None + # expected.location_path = "storage/new_res" + # + # exchange3 = (req, expected) + # self.current_mid += 1 + # + # self._test_with_client([exchange1, exchange2, exchange3]) + # + # def test_content_type(self): + # print "TEST_CONTENT_TYPE" + # path = "/storage/new_res" + # + # req = Request() + # req.code = defines.Codes.POST.number + # req.uri_path = path + # req.type = defines.Types["CON"] + # req._mid = self.current_mid + # req.destination = self.server_address + # req.payload = "test" + # req.content_type = defines.Content_types["application/xml"] + # + # expected = Response() + # expected.type = defines.Types["ACK"] + # expected._mid = self.current_mid + # expected.code = defines.Codes.CREATED.number + # expected.token = None + # expected.payload = None + # expected.location_path = "storage/new_res" + # + # exchange1 = (req, expected) + # self.current_mid += 1 + # + # req = Request() + # req.code = defines.Codes.GET.number + # req.uri_path = path + # req.type = defines.Types["CON"] + # req._mid = self.current_mid + # req.destination = self.server_address + # + # expected = Response() + # expected.type = defines.Types["ACK"] + # expected._mid = self.current_mid + # expected.code = defines.Codes.CONTENT.number + # expected.token = None + # expected.payload = "Basic Resource" + # + # exchange2 = (req, expected) + # self.current_mid += 1 + # + # req = Request() + # req.code = defines.Codes.PUT.number + # req.uri_path = path + # req.type = defines.Types["CON"] + # req._mid = self.current_mid + # req.destination = self.server_address + # req.payload = "test" + # + # expected = Response() + # expected.type = defines.Types["ACK"] + # expected._mid = self.current_mid + # expected.code = defines.Codes.CHANGED.number + # expected.token = None + # expected.payload = None + # + # exchange3 = (req, expected) + # self.current_mid += 1 + # + # req = Request() + # req.code = defines.Codes.GET.number + # req.uri_path = path + # req.type = defines.Types["CON"] + # req._mid = self.current_mid + # req.destination = self.server_address + # + # expected = Response() + # expected.type = defines.Types["ACK"] + # expected._mid = self.current_mid + # expected.code = defines.Codes.CONTENT.number + # expected.token = None + # expected.payload = "test" + # + # exchange4 = (req, expected) + # self.current_mid += 1 + # + # req = Request() + # req.code = defines.Codes.GET.number + # req.uri_path = path + # req.type = defines.Types["CON"] + # req._mid = self.current_mid + # req.destination = self.server_address + # req.accept = defines.Content_types["application/xml"] + # + # expected = Response() + # expected.type = defines.Types["ACK"] + # expected._mid = self.current_mid + # expected.code = defines.Codes.CONTENT.number + # expected.token = None + # expected.payload = "test" + # + # exchange5 = (req, expected) + # self.current_mid += 1 + # + # req = Request() + # req.code = defines.Codes.GET.number + # req.uri_path = path + # req.type = defines.Types["CON"] + # req._mid = self.current_mid + # req.destination = self.server_address + # req.accept = defines.Content_types["application/json"] + # + # expected = Response() + # expected.type = defines.Types["ACK"] + # expected._mid = self.current_mid + # expected.code = defines.Codes.NOT_ACCEPTABLE.number + # expected.token = None + # expected.payload = None + # expected.content_type = defines.Content_types["application/json"] + # + # exchange6 = (req, expected) + # self.current_mid += 1 + # + # req = Request() + # req.code = defines.Codes.GET.number + # req.uri_path = "/xml" + # req.type = defines.Types["CON"] + # req._mid = self.current_mid + # req.destination = self.server_address + # + # expected = Response() + # expected.type = defines.Types["ACK"] + # expected._mid = self.current_mid + # expected.code = defines.Codes.CONTENT.number + # expected.token = None + # expected.payload = "0" + # expected.content_type = defines.Content_types["application/xml"] + # + # print(expected.pretty_print()) + # + # exchange7 = (req, expected) + # self.current_mid += 1 + # + # self._test_with_client([exchange1, exchange2, exchange3, exchange4, exchange5, exchange6, exchange7]) + # + # def test_ETAG(self): + # print "TEST_ETAG" + # path = "/etag" + # + # req = Request() + # req.code = defines.Codes.GET.number + # req.uri_path = path + # req.type = defines.Types["CON"] + # req._mid = self.current_mid + # req.destination = self.server_address + # + # expected = Response() + # expected.type = defines.Types["ACK"] + # expected._mid = self.current_mid + # expected.code = defines.Codes.CONTENT.number + # expected.token = None + # expected.payload = "ETag resource" + # expected.etag = "0" + # + # exchange1 = (req, expected) + # self.current_mid += 1 + # + # req = Request() + # req.code = defines.Codes.POST.number + # req.uri_path = path + # req.type = defines.Types["CON"] + # req._mid = self.current_mid + # req.destination = self.server_address + # req.payload = "test" + # + # expected = Response() + # expected.type = defines.Types["ACK"] + # expected._mid = self.current_mid + # expected.code = defines.Codes.CREATED.number + # expected.token = None + # expected.payload = None + # expected.location_path = path + # expected.etag = "1" + # + # exchange2 = (req, expected) + # self.current_mid += 1 + # + # req = Request() + # req.code = defines.Codes.GET.number + # req.uri_path = path + # req.type = defines.Types["CON"] + # req._mid = self.current_mid + # req.destination = self.server_address + # req.etag = "1" + # + # expected = Response() + # expected.type = defines.Types["ACK"] + # expected._mid = self.current_mid + # expected.code = defines.Codes.VALID.number + # expected.token = None + # expected.payload = "test" + # expected.etag = "1" + # + # exchange3 = (req, expected) + # self.current_mid += 1 + # + # self._test_with_client([exchange1, exchange2, exchange3]) + # + # def test_child(self): + # print "TEST_CHILD" + # path = "/child" + # + # req = Request() + # req.code = defines.Codes.POST.number + # req.uri_path = path + # req.type = defines.Types["CON"] + # req._mid = self.current_mid + # req.destination = self.server_address + # req.payload = "test" + # + # expected = Response() + # expected.type = defines.Types["ACK"] + # expected._mid = self.current_mid + # expected.code = defines.Codes.CREATED.number + # expected.token = None + # expected.payload = None + # expected.location_path = path + # + # exchange1 = (req, expected) + # self.current_mid += 1 + # + # req = Request() + # req.code = defines.Codes.GET.number + # req.uri_path = path + # req.type = defines.Types["CON"] + # req._mid = self.current_mid + # req.destination = self.server_address + # + # expected = Response() + # expected.type = defines.Types["ACK"] + # expected._mid = self.current_mid + # expected.code = defines.Codes.CONTENT.number + # expected.token = None + # expected.payload = "test" + # + # exchange2 = (req, expected) + # self.current_mid += 1 + # + # req = Request() + # req.code = defines.Codes.PUT.number + # req.uri_path = path + # req.type = defines.Types["CON"] + # req._mid = self.current_mid + # req.destination = self.server_address + # req.payload = "testPUT" + # + # expected = Response() + # expected.type = defines.Types["ACK"] + # expected._mid = self.current_mid + # expected.code = defines.Codes.CHANGED.number + # expected.token = None + # expected.payload = None + # + # exchange3 = (req, expected) + # self.current_mid += 1 + # + # req = Request() + # req.code = defines.Codes.DELETE.number + # req.uri_path = path + # req.type = defines.Types["CON"] + # req._mid = self.current_mid + # req.destination = self.server_address + # + # expected = Response() + # expected.type = defines.Types["ACK"] + # expected._mid = self.current_mid + # expected.code = defines.Codes.DELETED.number + # expected.token = None + # expected.payload = None + # + # exchange4 = (req, expected) + # self.current_mid += 1 + # + # self._test_with_client([exchange1, exchange2, exchange3, exchange4]) + # + # def test_not_found(self): + # print "TEST_not_found" + # path = "/not_found" + # + # req = Request() + # req.code = defines.Codes.GET.number + # req.uri_path = path + # req.type = defines.Types["CON"] + # req._mid = self.current_mid + # req.destination = self.server_address + # req.token = 100 + # + # expected = Response() + # expected.type = defines.Types["ACK"] + # expected._mid = self.current_mid + # expected.code = defines.Codes.NOT_FOUND.number + # expected.token = "100" + # expected.payload = None + # + # exchange1 = (req, expected) + # self.current_mid += 1 + # + # req = Request() + # req.code = defines.Codes.POST.number + # req.uri_path = path + # req.type = defines.Types["CON"] + # req._mid = self.current_mid + # req.destination = self.server_address + # req.payload = "test" + # + # expected = Response() + # expected.type = defines.Types["ACK"] + # expected._mid = self.current_mid + # expected.code = defines.Codes.METHOD_NOT_ALLOWED.number + # expected.token = None + # + # exchange2 = (req, expected) + # self.current_mid += 1 + # + # req = Request() + # req.code = defines.Codes.PUT.number + # req.uri_path = path + # req.type = defines.Types["CON"] + # req._mid = self.current_mid + # req.destination = self.server_address + # req.payload = "testPUT" + # + # expected = Response() + # expected.type = defines.Types["ACK"] + # expected._mid = self.current_mid + # expected.code = defines.Codes.NOT_FOUND.number + # expected.token = None + # expected.payload = None + # + # exchange3 = (req, expected) + # self.current_mid += 1 + # + # req = Request() + # req.code = defines.Codes.DELETE.number + # req.uri_path = path + # req.type = defines.Types["CON"] + # req._mid = self.current_mid + # req.destination = self.server_address + # + # expected = Response() + # expected.type = defines.Types["ACK"] + # expected._mid = self.current_mid + # expected.code = defines.Codes.NOT_FOUND.number + # expected.token = None + # expected.payload = None + # + # exchange4 = (req, expected) + # self.current_mid += 1 + # + # self._test_with_client([exchange1, exchange2, exchange3, exchange4]) + # + # def test_invalid(self): + # print("TEST_INVALID") + # + # # version + # req = ("\x00\x01\x8c\xda", self.server_address) + # + # expected = Response() + # expected.type = defines.Types["RST"] + # expected._mid = None + # expected.code = defines.Codes.BAD_REQUEST.number + # + # exchange1 = (req, expected) + # + # # version + # req = ("\x40", self.server_address) + # + # expected = Response() + # expected.type = defines.Types["RST"] + # expected._mid = None + # expected.code = defines.Codes.BAD_REQUEST.number + # + # exchange2 = (req, expected) + # + # # code + # req = ("\x40\x05\x8c\xda", self.server_address) + # + # expected = Response() + # expected.type = defines.Types["RST"] + # expected._mid = None + # expected.code = defines.Codes.BAD_REQUEST.number + # + # exchange3 = (req, expected) + # + # # option + # req = ("\x40\x01\x8c\xda\x94", self.server_address) + # + # expected = Response() + # expected.type = defines.Types["RST"] + # expected._mid = None + # expected.code = defines.Codes.BAD_REQUEST.number + # + # exchange4 = (req, expected) + # + # # payload marker + # req = ("\x40\x02\x8c\xda\x75\x62\x61\x73\x69\x63\xff", self.server_address) + # + # expected = Response() + # expected.type = defines.Types["RST"] + # expected._mid = None + # expected.code = defines.Codes.BAD_REQUEST.number + # + # exchange5 = (req, expected) + # + # self._test_datagram([exchange1, exchange2, exchange3, exchange4, exchange5]) + + def test_post_block_big_client(self): + print("TEST_POST_BLOCK_BIG_CLIENT") + path = "/Server1/big" + req = Request() + + req.code = defines.Codes.POST.number + req.uri_path = path + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + req.payload = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras sollicitudin fermentum ornare. " \ + "Cras accumsan tellus quis dui lacinia eleifend. Proin ultrices rutrum orci vitae luctus. " \ + "Nullam malesuada pretium elit, at aliquam odio vehicula in. Etiam nec maximus elit. " \ + "Etiam at erat ac ex ornare feugiat. Curabitur sed malesuada orci, id aliquet nunc. Phasellus " \ + "nec leo luctus, blandit lorem sit amet, interdum metus. Duis efficitur volutpat magna, ac " \ + "ultricies nibh aliquet sit amet. Etiam tempor egestas augue in hendrerit. Nunc eget augue " \ + "ultricies, dignissim lacus et, vulputate dolor. Nulla eros odio, fringilla vel massa ut, " \ + "facilisis cursus quam. Fusce faucibus lobortis congue. Fusce consectetur porta neque, id " \ + "sollicitudin velit maximus eu. Sed pharetra leo quam, vel finibus turpis cursus ac. " \ + "Aenean ac nisi massa. Cras commodo arcu nec ante tristique ullamcorper. Quisque eu hendrerit" \ + " urna. Cras fringilla eros ut nunc maximus, non porta nisl mollis. Aliquam in rutrum massa." \ + " Praesent tristique turpis dui, at ultricies lorem fermentum at. Vivamus sit amet ornare neque, " \ + "a imperdiet nisl. Quisque a iaculis libero, id tempus lacus. Aenean convallis est non justo " \ + "consectetur, a hendrerit enim consequat. In accumsan ante a egestas luctus. Etiam quis neque " \ + "nec eros vestibulum faucibus. Nunc viverra ipsum lectus, vel scelerisque dui dictum a. Ut orci " \ + "enim, ultrices a ultrices nec, pharetra in quam. Donec accumsan sit amet eros eget fermentum." \ + "Vivamus ut odio ac odio malesuada accumsan. Aenean vehicula diam at tempus ornare. Phasellus " \ + "dictum mauris a mi consequat, vitae mattis nulla fringilla. Ut laoreet tellus in nisl efficitur," \ + " a luctus justo tempus. Fusce finibus libero eget velit finibus iaculis. Morbi rhoncus purus " \ + "vel vestibulum ullamcorper. Sed ac metus in urna fermentum feugiat. Nulla nunc diam, sodales " \ + "aliquam mi id, varius porta nisl. Praesent vel nibh ac turpis rutrum laoreet at non odio. " \ + "Phasellus ut posuere mi. Suspendisse malesuada velit nec mauris convallis porta. Vivamus " \ + "sed ultrices sapien, at cras amet." + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = None + expected.code = defines.Codes.CHANGED.number + expected.token = None + expected.payload = None + + exchange1 = (req, expected) + self.current_mid += 1 + + self._test_with_client([exchange1]) + + def test_observe_client(self): + print("TEST_OBSERVE_CLIENT") + path = "/Server1/basic" + + req = Request() + req.code = defines.Codes.GET.number + req.uri_path = path + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + req.observe = 0 + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = None + expected.code = defines.Codes.CONTENT.number + expected.token = None + expected.payload = None + + exchange1 = (req, expected) + + self._test_with_client_observe([exchange1]) + + def test_duplicate(self): + print("TEST_DUPLICATE") + path = "/Server1/basic" + req = Request() + req.code = defines.Codes.GET.number + req.uri_path = path + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = self.current_mid + expected.code = defines.Codes.CONTENT.number + expected.token = None + + self.current_mid += 1 + self._test_plugtest([(req, expected), (req, expected)]) + + def test_duplicate_not_completed(self): + print("TEST_DUPLICATE_NOT_COMPLETED") + path = "/Server1/long" + req = Request() + req.code = defines.Codes.GET.number + req.uri_path = path + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = self.current_mid + expected.code = None + expected.token = None + + expected2 = Response() + expected2.type = defines.Types["CON"] + expected2._mid = None + expected2.code = defines.Codes.CONTENT.number + expected2.token = None + + self.current_mid += 1 + self._test_plugtest([(req, None), (req, expected), (None, expected2)]) + +if __name__ == '__main__': + unittest.main() + diff --git a/dimmerserver.py b/dimmerserver.py new file mode 100644 index 0000000..27fb73e --- /dev/null +++ b/dimmerserver.py @@ -0,0 +1,74 @@ +from coapthon.resources.resource import Resource +from coapthon.server.coap import CoAP + + +class DimmerResource(Resource): + def __init__(self, name="DimmerResource", coap_server=None): + super(DimmerResource, self).__init__(name, coap_server, visible=True, + observable=True, allow_children=True) + self.value = 0 + self.payload = str(self.value) + self.resource_type = "dimmer" + self.content_type = "text/plain" + self.interface_type = "urn:oma:lwm2m:ext:3311:5851" + + def render_GET(self, request): + self.payload = str(self.value) + return self + + def render_PUT(self, request): + if request.payload.isdigit(): + amount = int(request.payload) + if 0 <= amount <= 100: + self.value = amount + self.edit_resource(request) + return self + return None + + +class SwitchResource(Resource): + def __init__(self, name="SwitchResource", coap_server=None): + super(SwitchResource, self).__init__(name, coap_server, visible=True, + observable=True, allow_children=True) + self.value = 0 + self.payload = str(self.value) + self.resource_type = "switch" + self.content_type = "text/plain" + self.interface_type = "urn:oma:lwm2m:ext:3311:5850" + + def render_GET(self, request): + self.payload = str(self.value) + return self + + def render_PUT(self, request): + if request.payload.isdigit(): + status = int(request.payload) + if 0 <= status <= 1: + self.value = status + self.edit_resource(request) + return self + return None + + +class CoAPServer(CoAP): + def __init__(self, host, port, multicast=False): + CoAP.__init__(self, (host, port), multicast) + self.add_resource('dimmer/', DimmerResource()) + self.add_resource('switch/', SwitchResource()) + + +def main(): # pragma: no cover + ip = "127.0.0.1" + port = 5683 + + server = CoAPServer(ip, port, False) + try: + server.listen(10) + except KeyboardInterrupt: + print("Server Shutdown") + server.close() + print("Exiting...") + + +if __name__ == "__main__": # pragma: no cover + main() diff --git a/exampleresources.py b/exampleresources.py new file mode 100644 index 0000000..0330829 --- /dev/null +++ b/exampleresources.py @@ -0,0 +1,281 @@ +import time +from coapthon import defines + +from coapthon.resources.resource import Resource + +__author__ = 'Giacomo Tanganelli' + + +class BasicResource(Resource): + def __init__(self, name="BasicResource", coap_server=None): + super(BasicResource, self).__init__(name, coap_server, visible=True, + observable=True, allow_children=True) + self.payload = "Basic Resource" + self.resource_type = "rt1" + self.content_type = "text/plain" + self.interface_type = "if1" + + def render_GET(self, request): + return self + + def render_PUT(self, request): + self.edit_resource(request) + return self + + def render_POST(self, request): + res = self.init_resource(request, BasicResource()) + return res + + def render_DELETE(self, request): + return True + + +class Storage(Resource): + def __init__(self, name="StorageResource", coap_server=None): + super(Storage, self).__init__(name, coap_server, visible=True, observable=True, allow_children=True) + self.payload = "Storage Resource for PUT, POST and DELETE" + + def render_GET(self, request): + return self + + def render_POST(self, request): + res = self.init_resource(request, BasicResource()) + return res + + +class Child(Resource): + def __init__(self, name="ChildResource", coap_server=None): + super(Child, self).__init__(name, coap_server, visible=True, observable=True, allow_children=True) + self.payload = "" + + def render_GET(self, request): + return self + + def render_PUT(self, request): + self.payload = request.payload + return self + + def render_POST(self, request): + res = BasicResource() + res.location_query = request.uri_query + res.payload = request.payload + return res + + def render_DELETE(self, request): + return True + + +class Separate(Resource): + + def __init__(self, name="Separate", coap_server=None): + super(Separate, self).__init__(name, coap_server, visible=True, observable=True, allow_children=True) + self.payload = "Separate" + self.max_age = 60 + + def render_GET(self, request): + return self, self.render_GET_separate + + def render_GET_separate(self, request): + time.sleep(5) + return self + + def render_POST(self, request): + return self, self.render_POST_separate + + def render_POST_separate(self, request): + self.payload = request.payload + return self + + def render_PUT(self, request): + return self, self.render_PUT_separate + + def render_PUT_separate(self, request): + self.payload = request.payload + return self + + def render_DELETE(self, request): + return self, self.render_DELETE_separate + + def render_DELETE_separate(self, request): + return True + + +class Long(Resource): + + def __init__(self, name="Long", coap_server=None): + super(Long, self).__init__(name, coap_server, visible=True, observable=True, allow_children=True) + self.payload = "Long Time" + + def render_GET(self, request): + time.sleep(10) + return self + + +class Big(Resource): + + def __init__(self, name="Big", coap_server=None): + super(Big, self).__init__(name, coap_server, visible=True, observable=True, allow_children=True) + self.payload = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras sollicitudin fermentum ornare. " \ + "Cras accumsan tellus quis dui lacinia eleifend. Proin ultrices rutrum orci vitae luctus. " \ + "Nullam malesuada pretium elit, at aliquam odio vehicula in. Etiam nec maximus elit. " \ + "Etiam at erat ac ex ornare feugiat. Curabitur sed malesuada orci, id aliquet nunc. Phasellus " \ + "nec leo luctus, blandit lorem sit amet, interdum metus. Duis efficitur volutpat magna, ac " \ + "ultricies nibh aliquet sit amet. Etiam tempor egestas augue in hendrerit. Nunc eget augue " \ + "ultricies, dignissim lacus et, vulputate dolor. Nulla eros odio, fringilla vel massa ut, " \ + "facilisis cursus quam. Fusce faucibus lobortis congue. Fusce consectetur porta neque, id " \ + "sollicitudin velit maximus eu. Sed pharetra leo quam, vel finibus turpis cursus ac. " \ + "Aenean ac nisi massa. Cras commodo arcu nec ante tristique ullamcorper. Quisque eu hendrerit" \ + " urna. Cras fringilla eros ut nunc maximus, non porta nisl mollis. Aliquam in rutrum massa." \ + " Praesent tristique turpis dui, at ultricies lorem fermentum at. Vivamus sit amet ornare neque, " \ + "a imperdiet nisl. Quisque a iaculis libero, id tempus lacus. Aenean convallis est non justo " \ + "consectetur, a hendrerit enim consequat. In accumsan ante a egestas luctus. Etiam quis neque " \ + "nec eros vestibulum faucibus. Nunc viverra ipsum lectus, vel scelerisque dui dictum a. Ut orci " \ + "enim, ultrices a ultrices nec, pharetra in quam. Donec accumsan sit amet eros eget fermentum." \ + "Vivamus ut odio ac odio malesuada accumsan. Aenean vehicula diam at tempus ornare. Phasellus " \ + "dictum mauris a mi consequat, vitae mattis nulla fringilla. Ut laoreet tellus in nisl efficitur," \ + " a luctus justo tempus. Fusce finibus libero eget velit finibus iaculis. Morbi rhoncus purus " \ + "vel vestibulum ullamcorper. Sed ac metus in urna fermentum feugiat. Nulla nunc diam, sodales " \ + "aliquam mi id, varius porta nisl. Praesent vel nibh ac turpis rutrum laoreet at non odio. " \ + "Phasellus ut posuere mi. Suspendisse malesuada velit nec mauris convallis porta. Vivamus " \ + "sed ultrices sapien, at cras amet." + + def render_GET(self, request): + return self + + def render_POST(self, request): + if request.payload is not None: + self.payload = request.payload + return self + + +class voidResource(Resource): + def __init__(self, name="Void"): + super(voidResource, self).__init__(name) + + +class XMLResource(Resource): + def __init__(self, name="XML"): + super(XMLResource, self).__init__(name) + self.value = 0 + self.payload = (defines.Content_types["application/xml"], ""+str(self.value)+"") + + def render_GET(self, request): + return self + + +class MultipleEncodingResource(Resource): + def __init__(self, name="MultipleEncoding"): + super(MultipleEncodingResource, self).__init__(name) + self.value = 0 + self.payload = str(self.value) + self.content_type = [defines.Content_types["application/xml"], defines.Content_types["application/json"]] + + def render_GET(self, request): + if request.accept == defines.Content_types["application/xml"]: + self.payload = (defines.Content_types["application/xml"], ""+str(self.value)+"") + elif request.accept == defines.Content_types["application/json"]: + self.payload = (defines.Content_types["application/json"], "{'value': '"+str(self.value)+"'}") + elif request.accept == defines.Content_types["text/plain"]: + self.payload = (defines.Content_types["text/plain"], str(self.value)) + return self + + def render_PUT(self, request): + self.edit_resource(request) + return self + + def render_POST(self, request): + res = self.init_resource(request, MultipleEncodingResource()) + return res + + +class ETAGResource(Resource): + def __init__(self, name="ETag"): + super(ETAGResource, self).__init__(name) + self.count = 0 + self.payload = "ETag resource" + self.etag = str(self.count) + + def render_GET(self, request): + return self + + def render_POST(self, request): + self.payload = request.payload + self.count += 1 + self.etag = str(self.count) + return self + + def render_PUT(self, request): + self.payload = request.payload + return self + + +class AdvancedResource(Resource): + def __init__(self, name="Advanced"): + super(AdvancedResource, self).__init__(name) + self.payload = "Advanced resource" + + def render_GET_advanced(self, request, response): + response.payload = self.payload + response.max_age = 20 + response.code = defines.Codes.CONTENT.number + return self, response + + def render_POST_advanced(self, request, response): + self.payload = request.payload + from coapthon.messages.response import Response + assert(isinstance(response, Response)) + response.payload = "Response changed through POST" + response.code = defines.Codes.CREATED.number + return self, response + + def render_PUT_advanced(self, request, response): + self.payload = request.payload + from coapthon.messages.response import Response + assert(isinstance(response, Response)) + response.payload = "Response changed through PUT" + response.code = defines.Codes.CHANGED.number + return self, response + + def render_DELETE_advanced(self, request, response): + response.payload = "Response deleted" + response.code = defines.Codes.DELETED.number + return True, response + + +class AdvancedResourceSeparate(Resource): + def __init__(self, name="Advanced"): + super(AdvancedResourceSeparate, self).__init__(name) + self.payload = "Advanced resource" + + def render_GET_advanced(self, request, response): + return self, response, self.render_GET_separate + + def render_POST_advanced(self, request, response): + return self, response, self.render_POST_separate + + def render_PUT_advanced(self, request, response): + + return self, response, self.render_PUT_separate + + def render_DELETE_advanced(self, request, response): + return self, response, self.render_DELETE_separate + + def render_GET_separate(self, request, response): + time.sleep(5) + response.payload = self.payload + response.max_age = 20 + return self, response + + def render_POST_separate(self, request, response): + self.payload = request.payload + response.payload = "Response changed through POST" + return self, response + + def render_PUT_separate(self, request, response): + self.payload = request.payload + response.payload = "Response changed through PUT" + return self, response + + def render_DELETE_separate(self, request, response): + response.payload = "Response deleted" + return True, response diff --git a/logging.conf b/logging.conf new file mode 100644 index 0000000..86a74a0 --- /dev/null +++ b/logging.conf @@ -0,0 +1,22 @@ +[loggers] +keys=root + +[handlers] +keys=consoleHandler + +[formatters] +keys=simpleFormatter + +[logger_root] +level=DEBUG +handlers=consoleHandler + +[handler_consoleHandler] +class=StreamHandler +level=DEBUG +formatter=simpleFormatter +args=(sys.stdout,) + +[formatter_simpleFormatter] +format=%(asctime)s - %(threadName)-10s - %(name)s - %(levelname)s - %(message)s +datefmt= \ No newline at end of file diff --git a/lorem_impsum.txt b/lorem_impsum.txt new file mode 100644 index 0000000..f0bebe4 --- /dev/null +++ b/lorem_impsum.txt @@ -0,0 +1,7 @@ +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque neque lorem, congue sit amet fringilla sit amet, varius eget leo. Integer elementum eget odio sed consequat. Nullam sed ligula ex. Morbi scelerisque eros eget hendrerit egestas. Quisque feugiat massa vel nulla viverra eleifend. Duis quis augue lacus. Nullam id tortor ac eros porttitor bibendum eget a ex. Nulla sed ante placerat, efficitur eros scelerisque, cursus tellus. + +Nam nec erat semper quam condimentum congue nec ac magna. Phasellus nisl risus, commodo id quam vitae, feugiat posuere ex. Mauris volutpat ligula nec dolor aliquet, sed tempus leo venenatis. Nulla at finibus purus, id fermentum magna. Mauris vitae porta risus, eleifend iaculis augue. Nulla eu velit dui. Phasellus commodo rhoncus varius. Duis pretium libero odio, a elementum turpis condimentum eu. Vestibulum laoreet odio id lorem finibus, eget volutpat arcu porta. Nam non odio ac lectus lobortis facilisis. + +Suspendisse potenti. Aenean euismod ipsum in erat euismod, in cursus nibh tempor. Donec dictum pulvinar elit nec malesuada. Ut id maximus massa, et vestibulum justo. In vel nisl imperdiet, faucibus tortor et, hendrerit risus. Nullam in semper massa. Vestibulum rutrum augue vitae tortor rhoncus cursus. Aenean sed varius nisi, vel egestas lorem. Praesent malesuada tellus id elit sodales, vitae accumsan metus bibendum. Sed tincidunt faucibus ante, pellentesque viverra dui tristique et. + +Phasellus ornare risus sed tincidunt aliquet. Praesent sodales blandit ligula vel posuere. Nullam metus neque, maximus vitae congue in, commodo non ex. Proin euismod efficitur lectus id rutrum. Duis non eros sed massa euismod efficitur. Nam vitae lacus pharetra, imperdiet lorem eu, vestibulum quam. Praesent vitae sem tincidunt massa gravida condimentum. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Praesent elementum lorem sed velit gravida bibendum vitae non elit. Nunc iaculis iaculis ultricies. Mauris mattis vehicula aliquet. Donec dui nisl, vestibulum eget rutrum vel, hendrerit eu enim posuere. \ No newline at end of file diff --git a/plugtest.py b/plugtest.py new file mode 100644 index 0000000..eb764c5 --- /dev/null +++ b/plugtest.py @@ -0,0 +1,889 @@ +# -*- coding: utf-8 -*- +from queue import Queue +import random +import socket +import threading +import unittest +from coapthon.messages.message import Message +from coapclient import HelperClient +from coapthon.messages.response import Response +from coapthon.messages.request import Request +from coapthon import defines +from coapthon.serializer import Serializer +from plugtest_coapserver import CoAPServerPlugTest + +__author__ = 'Giacomo Tanganelli' +__version__ = "2.0" + + +class Tests(unittest.TestCase): + + def setUp(self): + self.server_address = ("127.0.0.1", 5683) + self.current_mid = random.randint(1, 1000) + self.server_mid = random.randint(1000, 2000) + self.server = CoAPServerPlugTest("127.0.0.1", 5683, starting_mid=self.server_mid) + self.server_thread = threading.Thread(target=self.server.listen, args=(10,)) + self.server_thread.start() + self.queue = Queue() + + def tearDown(self): + self.server.close() + self.server_thread.join(timeout=25) + self.server = None + + def _test_with_client(self, message_list): # pragma: no cover + client = HelperClient(self.server_address) + for message, expected in message_list: + if message is not None: + received_message = client.send_request(message) + if expected is not None: + # print(received_message.payload) + # print(expected.payload) + if expected.type is not None: + self.assertEqual(received_message.type, expected.type) + if expected.mid is not None: + self.assertEqual(received_message.mid, expected.mid) + self.assertEqual(received_message.code, expected.code) + if expected.source is not None: + self.assertEqual(received_message.source, self.server_address) + if expected.token is not None: + self.assertEqual(received_message.token, expected.token) + if expected.payload is not None: + self.assertEqual(received_message.payload, expected.payload) + if expected.options is not None: + self.assertEqual(received_message.options, expected.options) + for o in expected.options: + option_value = getattr(expected, o.name.lower().replace("-", "_")) + option_value_rec = getattr(received_message, o.name.lower().replace("-", "_")) + self.assertEqual(option_value, option_value_rec) + client.stop() + + def client_callback(self, response): + print("Callback") + self.queue.put(response) + + def _test_with_client_observe(self, message_list, callback): # pragma: no cover + client = HelperClient(self.server_address) + token = None + last_mid = 0 + for message, expected in message_list: + if message is not None: + token = message.token + client.send_request(message, callback) + received_message = self.queue.get() + if expected is not None: + last_mid = expected.mid + if expected.type is not None: + self.assertEqual(received_message.type, expected.type) + if expected.mid is not None: + self.assertEqual(received_message.mid, expected.mid) + self.assertEqual(received_message.code, expected.code) + if expected.source is not None: + self.assertEqual(received_message.source, self.server_address) + if expected.token is not None: + self.assertEqual(received_message.token, expected.token) + if expected.payload is not None: + self.assertEqual(received_message.payload, expected.payload) + if expected.options is not None: + self.assertEqual(received_message.options, expected.options) + message = Message() + message.type = defines.Types["RST"] + message.token = token + message._mid = last_mid + message.destination = self.server_address + client.send_empty(message) + client.stop() + + def _test_plugtest(self, message_list): # pragma: no cover + serializer = Serializer() + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + for message, expected in message_list: + if message is not None: + datagram = serializer.serialize(message) + sock.sendto(datagram, message.destination) + if expected is not None: + datagram, source = sock.recvfrom(4096) + received_message = serializer.deserialize(datagram, source) + if expected.type is not None: + self.assertEqual(received_message.type, expected.type) + if expected.mid is not None: + self.assertEqual(received_message.mid, expected.mid) + self.assertEqual(received_message.code, expected.code) + if expected.source is not None: + self.assertEqual(received_message.source, source) + if expected.token is not None: + self.assertEqual(received_message.token, expected.token) + if expected.payload is not None: + self.assertEqual(received_message.payload, expected.payload) + if expected.options is not None: + self.assertEqual(received_message.options, expected.options) + for o in expected.options: + option_value = getattr(expected, o.name.lower().replace("-", "_")) + option_value_rec = getattr(received_message, o.name.lower().replace("-", "_")) + self.assertEqual(option_value, option_value_rec) + sock.close() + + def test_td_coap_link_01(self): + print("TD_COAP_LINK_01") + path = "/.well-known/core" + req = Request() + req.code = defines.Codes.GET.number + req.uri_path = path + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = self.current_mid + expected.code = defines.Codes.CONTENT.number + expected.token = None + expected.content_type = defines.Content_types["application/link-format"] + expected.payload = """;;;;obs,
;rt="Type1";sz="13",;rt="Type1";sz="13",;rt="Type1";sz="13",;rt="Type1";sz="13",;ct=0;if="separate",;rt="Type1";sz="13",""" + self.current_mid += 1 + self._test_with_client([(req, expected)]) + + def test_td_coap_link_02(self): + print("TD_COAP_LINK_02") + path = "/.well-known/core" + req = Request() + req.code = defines.Codes.GET.number + req.uri_path = path + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + req.uri_query = "rt=Type1" + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = self.current_mid + expected.code = defines.Codes.CONTENT.number + expected.token = None + expected.content_type = defines.Content_types["application/link-format"] + expected.payload = """
;rt="Type1";sz="13",;rt="Type1";sz="13",;rt="Type1";sz="13",;rt="Type1";sz="13",;rt="Type1";sz="13",""" + self.current_mid += 1 + self._test_with_client([(req, expected)]) + + def test_td_coap_core_01(self): + print("TD_COAP_CORE_01") + path = "/test" + req = Request() + req.code = defines.Codes.GET.number + req.uri_path = path + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = self.current_mid + expected.code = defines.Codes.CONTENT.number + expected.token = None + expected.payload = "Test Resource" + + self.current_mid += 1 + self._test_with_client([(req, expected)]) + + def test_td_coap_core_02(self): + print("TD_COAP_CORE_02") + path = "/test_post" + req = Request() + + req.code = defines.Codes.POST.number + req.uri_path = path + req.type = defines.Types["CON"] + req.content_type = defines.Content_types["application/xml"] + req._mid = self.current_mid + req.destination = self.server_address + req.payload = "test" + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = self.current_mid + expected.code = defines.Codes.CREATED.number + expected.token = None + expected.payload = None + expected.location_path = "/test_post" + + self.current_mid += 1 + self._test_with_client([(req, expected)]) + + def test_td_coap_core_03(self): + print("TD_COAP_CORE_03") + path = "/test" + req = Request() + + req.code = defines.Codes.PUT.number + req.uri_path = path + req.type = defines.Types["CON"] + req.content_type = defines.Content_types["application/xml"] + req._mid = self.current_mid + req.destination = self.server_address + req.payload = "test" + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = self.current_mid + expected.code = defines.Codes.CHANGED.number + expected.token = None + expected.payload = None + + self.current_mid += 1 + exchange1 = (req, expected) + + req = Request() + + req.code = defines.Codes.GET.number + req.uri_path = path + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = self.current_mid + expected.code = defines.Codes.CONTENT.number + expected.token = None + expected.payload = "Test Resource" + + self.current_mid += 1 + exchange2 = (req, expected) + + req = Request() + + req.code = defines.Codes.GET.number + req.uri_path = path + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + req.accept = defines.Content_types["application/xml"] + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = self.current_mid + expected.code = defines.Codes.CONTENT.number + expected.token = None + expected.payload = "test" + expected.content_type = defines.Content_types["application/xml"] + + self.current_mid += 1 + exchange3 = (req, expected) + self._test_with_client([exchange1, exchange2, exchange3]) + + def test_td_coap_core_04(self): + print("TD_COAP_CORE_04") + path = "/test" + req = Request() + + req.code = defines.Codes.DELETE.number + req.uri_path = path + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = self.current_mid + expected.code = defines.Codes.DELETED.number + expected.token = None + expected.payload = None + + self.current_mid += 1 + self._test_with_client([(req, expected)]) + + def test_td_coap_core_05(self): + print("TD_COAP_CORE_05") + path = "/test" + req = Request() + + req.code = defines.Codes.GET.number + req.uri_path = path + req.type = defines.Types["NON"] + req._mid = self.current_mid + req.destination = self.server_address + + expected = Response() + expected.type = defines.Types["NON"] + expected._mid = None + expected.code = defines.Codes.CONTENT.number + expected.token = None + expected.payload = "Test Resource" + + self.current_mid += 1 + self._test_with_client([(req, expected)]) + + def test_td_coap_core_06(self): + print("TD_COAP_CORE_06") + path = "/test_post" + req = Request() + + req.code = defines.Codes.POST.number + req.uri_path = path + req.type = defines.Types["NON"] + req.content_type = defines.Content_types["application/xml"] + req._mid = self.current_mid + req.destination = self.server_address + req.payload = "test" + + expected = Response() + expected.type = defines.Types["NON"] + expected._mid = None + expected.code = defines.Codes.CREATED.number + expected.token = None + expected.payload = None + expected.location_path = "/test_post" + + self.current_mid += 1 + self._test_with_client([(req, expected)]) + + def test_td_coap_core_07(self): + print("TD_COAP_CORE_07") + path = "/test" + req = Request() + + req.code = defines.Codes.PUT.number + req.uri_path = path + req.type = defines.Types["NON"] + req.content_type = defines.Content_types["application/xml"] + req._mid = self.current_mid + req.destination = self.server_address + req.payload = "test" + + expected = Response() + expected.type = defines.Types["NON"] + expected._mid = None + expected.code = defines.Codes.CHANGED.number + expected.token = None + expected.payload = None + + self.current_mid += 1 + self._test_with_client([(req, expected)]) + + def test_td_coap_core_08(self): + print("TD_COAP_CORE_08") + path = "/test" + req = Request() + + req.code = defines.Codes.DELETE.number + req.uri_path = path + req.type = defines.Types["NON"] + req._mid = self.current_mid + req.destination = self.server_address + + expected = Response() + expected.type = defines.Types["NON"] + expected._mid = None + expected.code = defines.Codes.DELETED.number + expected.token = None + expected.payload = None + + self.current_mid += 1 + self._test_with_client([(req, expected)]) + + def test_td_coap_core_09(self): + print("TD_COAP_CORE_09") + path = "/separate" + req = Request() + + req.code = defines.Codes.GET.number + req.uri_path = path + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = self.current_mid + expected.code = None + expected.token = None + expected.payload = None + + expected2 = Response() + expected2.type = defines.Types["CON"] + expected2._mid = self.server_mid + expected2.code = defines.Codes.CONTENT.number + expected2.token = None + expected2.payload = "Separate Resource" + + self.current_mid += 1 + self._test_plugtest([(req, expected), (None, expected2)]) + + def test_td_coap_core_10(self): + print("TD_COAP_CORE_10") + path = "/test" + req = Request() + + req.code = defines.Codes.GET.number + req.uri_path = path + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + req.token = "ciao" + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = self.current_mid + expected.code = defines.Codes.CONTENT.number + expected.token = None + expected.payload = "Test Resource" + expected.token = "ciao" + + self.current_mid += 1 + self._test_with_client([(req, expected)]) + + def test_td_coap_core_12(self): + print("TD_COAP_CORE_12") + path = "/seg1/seg2/seg3" + req = Request() + + req.code = defines.Codes.GET.number + req.uri_path = path + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = self.current_mid + expected.code = defines.Codes.CONTENT.number + expected.payload = "Test Resource" + + self.current_mid += 1 + self._test_with_client([(req, expected)]) + + def test_td_coap_core_13(self): + print("TD_COAP_CORE_13") + path = "/query?first=1&second=2&third=3" + req = Request() + + req.code = defines.Codes.GET.number + req.uri_path = path + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = self.current_mid + expected.code = defines.Codes.CONTENT.number + expected.token = None + expected.payload = "Test Resource" + + self.current_mid += 1 + self._test_with_client([(req, expected)]) + + def test_td_coap_obs_01(self): + print("TD_COAP_OBS_01") + path = "/obs" + req = Request() + + req.code = defines.Codes.GET.number + req.uri_path = path + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + req.observe = 0 + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = self.current_mid + expected.code = defines.Codes.CONTENT.number + expected.token = None + expected.payload = "Observable Resource" + expected.observe = 1 + + expected2 = Response() + expected2.type = defines.Types["CON"] + expected2._mid = self.server_mid + expected2.code = defines.Codes.CONTENT.number + expected2.token = None + expected2.payload = "Observable Resource" + expected2.observe = 1 + + self.current_mid += 1 + self.server_mid += 1 + self._test_plugtest([(req, expected), (None, expected2)]) + + def test_td_coap_obs_03(self): + print("TD_COAP_OBS_03") + path = "/obs" + req = Request() + + req.code = defines.Codes.GET.number + req.uri_path = path + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + req.observe = 0 + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = self.current_mid + expected.code = defines.Codes.CONTENT.number + expected.token = None + expected.payload = "Observable Resource" + expected.observe = 1 + + self.current_mid += 1 + + expected2 = Response() + expected2.type = defines.Types["CON"] + expected2._mid = self.server_mid + expected2.code = defines.Codes.CONTENT.number + expected2.token = None + expected2.payload = "Observable Resource" + expected2.observe = 1 + + rst = Response() + rst.type = defines.Types["RST"] + rst._mid = self.server_mid + rst.code = defines.Codes.EMPTY.number + rst.destination = self.server_address + rst.token = None + rst.payload = None + + self.current_mid += 1 + self.server_mid += 1 + self._test_plugtest([(req, expected), (None, expected2), (rst, None)]) + + def test_td_coap_block_01(self): + print("TD_COAP_BLOCK_01") + path = "/large" + + req = Request() + req.code = defines.Codes.GET.number + req.uri_path = path + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + req.block2 = (0, 0, 1024) + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = self.current_mid + expected.code = defines.Codes.CONTENT.number + expected.token = None + expected.payload = None + expected.block2 = (0, 1, 1024) + + exchange1 = (req, expected) + self.current_mid += 1 + self.server_mid += 1 + + req = Request() + req.code = defines.Codes.GET.number + req.uri_path = path + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + req.block2 = (1, 0, 1024) + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = self.current_mid + expected.code = defines.Codes.CONTENT.number + expected.token = None + expected.payload = None + expected.block2 = (1, 0, 1024) + + exchange2 = (req, expected) + self.current_mid += 1 + self.server_mid += 1 + + self._test_plugtest([exchange1, exchange2]) + + def test_td_coap_block_01_client(self): + print("TD_COAP_BLOCK_01") + path = "/large" + + req = Request() + req.code = defines.Codes.GET.number + req.uri_path = path + req.type = defines.Types["CON"] + req._mid = None + req.destination = self.server_address + req.block2 = (0, 0, 1024) + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = None + expected.code = defines.Codes.CONTENT.number + expected.token = None + expected.payload = """"Me sabbee plenty"—grunted Queequeg, puffing away at his pipe and sitting up in bed. +"You gettee in," he added, motioning to me with his tomahawk, and throwing the clothes to one side. He really did this +in not only a civil but a really kind and charitable way. I stood looking at him a moment. For all his tattooings +he was on the whole a clean, comely looking cannibal. What's all this fuss I have been making about, thought I to +myself—the man's a human being just as I am: he has just as much reason to fear me, as I have to be afraid of him. +Better sleep with a sober cannibal than a drunken Christian. +"Landlord," said I, "tell him to stash his tomahawk there, or pipe, or whatever you call it; tell him to stop smoking, +in short, and I will turn in with him. But I don't fancy having a man smoking in bed with me. It's dangerous. Besides, +I ain't insured." +This being told to Queequeg, he at once complied, and again politely motioned me to get into bed—rolling over to one +side as much as to say—"I won't touch a leg of ye." +"Good night, landlord," said I, "you may go." +I turned in, and never slept better in my life. +Upon waking next morning about daylight, I found Queequeg's arm thrown over me in the most loving and affectionate +manner. You had almost thought I had been his wife. The counterpane was of patchwork, full of odd little +parti-coloured squares and triangles; and this arm of his tattooed all over with an interminable Cretan labyrinth +of a figure, no two parts of which were of one precise shade—owing I suppose to his keeping his arm at sea +unmethodically in sun and shade, his shirt sleeves irregularly rolled up at various times—this same arm of his, +I say, looked for all the world like a strip of that same patchwork quilt. Indeed, partly lying on it as the arm did + when I first awoke, I could hardly tell it from the quilt, they so blended their hues together; and it was only by + the sense of weight and pressure that I could tell that Queequeg was hugging""" + expected.block2 = (1, 0, 1024) + + exchange1 = (req, expected) + self.current_mid += 1 + self.server_mid += 1 + + self._test_with_client([exchange1]) + + def test_td_coap_block_02_client(self): + print("TD_COAP_BLOCK_02") + path = "/large" + + req = Request() + req.code = defines.Codes.GET.number + req.uri_path = path + req.type = defines.Types["CON"] + req._mid = None + req.destination = self.server_address + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = None + expected.code = defines.Codes.CONTENT.number + expected.token = None + expected.payload = """"Me sabbee plenty"—grunted Queequeg, puffing away at his pipe and sitting up in bed. +"You gettee in," he added, motioning to me with his tomahawk, and throwing the clothes to one side. He really did this +in not only a civil but a really kind and charitable way. I stood looking at him a moment. For all his tattooings +he was on the whole a clean, comely looking cannibal. What's all this fuss I have been making about, thought I to +myself—the man's a human being just as I am: he has just as much reason to fear me, as I have to be afraid of him. +Better sleep with a sober cannibal than a drunken Christian. +"Landlord," said I, "tell him to stash his tomahawk there, or pipe, or whatever you call it; tell him to stop smoking, +in short, and I will turn in with him. But I don't fancy having a man smoking in bed with me. It's dangerous. Besides, +I ain't insured." +This being told to Queequeg, he at once complied, and again politely motioned me to get into bed—rolling over to one +side as much as to say—"I won't touch a leg of ye." +"Good night, landlord," said I, "you may go." +I turned in, and never slept better in my life. +Upon waking next morning about daylight, I found Queequeg's arm thrown over me in the most loving and affectionate +manner. You had almost thought I had been his wife. The counterpane was of patchwork, full of odd little +parti-coloured squares and triangles; and this arm of his tattooed all over with an interminable Cretan labyrinth +of a figure, no two parts of which were of one precise shade—owing I suppose to his keeping his arm at sea +unmethodically in sun and shade, his shirt sleeves irregularly rolled up at various times—this same arm of his, +I say, looked for all the world like a strip of that same patchwork quilt. Indeed, partly lying on it as the arm did + when I first awoke, I could hardly tell it from the quilt, they so blended their hues together; and it was only by + the sense of weight and pressure that I could tell that Queequeg was hugging""" + expected.block2 = (1, 0, 1024) + + exchange1 = (req, expected) + self.current_mid += 1 + self.server_mid += 1 + + self._test_with_client([exchange1]) + + def test_td_coap_block_02(self): + print("TD_COAP_BLOCK_02") + path = "/large" + + req = Request() + req.code = defines.Codes.GET.number + req.uri_path = path + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = self.current_mid + expected.code = defines.Codes.CONTENT.number + expected.token = None + expected.payload = None + expected.block2 = (0, 1, 1024) + + exchange1 = (req, expected) + self.current_mid += 1 + self.server_mid += 1 + + req = Request() + req.code = defines.Codes.GET.number + req.uri_path = path + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + req.block2 = (1, 0, 1024) + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = self.current_mid + expected.code = defines.Codes.CONTENT.number + expected.token = None + expected.payload = None + expected.block2 = (1, 0, 1024) + + exchange2 = (req, expected) + self.current_mid += 1 + self.server_mid += 1 + + self._test_plugtest([exchange1, exchange2]) + + def test_td_coap_block_03(self): + print("TD_COAP_BLOCK_03") + path = "/large-update" + + req = Request() + req.code = defines.Codes.PUT.number + req.uri_path = path + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + req.payload = """"Me sabbee plenty"—grunted Queequeg, puffing away at his pipe """ + req.block1 = (0, 1, 64) + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = self.current_mid + expected.code = defines.Codes.CONTINUE.number + expected.token = None + expected.payload = None + expected.block1 = (0, 1, 64) + + exchange1 = (req, expected) + self.current_mid += 1 + self.server_mid += 1 + + req = Request() + req.code = defines.Codes.PUT.number + req.uri_path = path + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + req.payload = """and sitting up in bed. "You gettee in," he added, motioning""" + req.block1 = (1, 0, 64) + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = self.current_mid + expected.code = defines.Codes.CHANGED.number + expected.token = None + expected.payload = None + + exchange2 = (req, expected) + self.current_mid += 1 + self.server_mid += 1 + + req = Request() + req.code = defines.Codes.GET.number + req.uri_path = path + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = self.current_mid + expected.code = defines.Codes.CONTENT.number + expected.token = None + expected.payload = """"Me sabbee plenty"—grunted Queequeg, puffing away at his pipe and sitting up in bed. "You gettee in," he added, motioning""" + + exchange3 = (req, expected) + self.current_mid += 1 + + self._test_plugtest([exchange1, exchange2, exchange3]) + + def test_duplicate(self): + print("TEST_DUPLICATE") + path = "/test" + req = Request() + req.code = defines.Codes.GET.number + req.uri_path = path + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = self.current_mid + expected.code = defines.Codes.CONTENT.number + expected.token = None + + self.current_mid += 1 + self._test_plugtest([(req, expected), (req, expected)]) + + def test_duplicate_not_completed(self): + print("TEST_DUPLICATE_NOT_COMPLETED") + path = "/long" + req = Request() + req.code = defines.Codes.GET.number + req.uri_path = path + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = self.current_mid + expected.code = None + expected.token = None + + expected2 = Response() + expected2.type = defines.Types["CON"] + expected2._mid = None + expected2.code = defines.Codes.CONTENT.number + expected2.token = None + + self.current_mid += 1 + self._test_plugtest([(req, None), (req, expected), (None, expected2)]) + + def test_no_response(self): + print("TEST_NO_RESPONSE") + path = "/long" + req = Request() + req.code = defines.Codes.GET.number + req.uri_path = path + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = self.current_mid + expected.code = None + expected.token = None + + expected2 = Response() + expected2.type = defines.Types["CON"] + expected2._mid = None + expected2.code = defines.Codes.CONTENT.number + expected2.token = None + + self.current_mid += 1 + self._test_plugtest([(req, expected), (None, expected2), (None, expected2), (None, expected2)]) + + def test_edit_resource(self): + print("TEST_EDIT_RESOURCE") + path = "/obs" + req = Request() + + req.code = defines.Codes.POST.number + req.uri_path = path + req.type = defines.Types["CON"] + req._mid = self.current_mid + req.destination = self.server_address + req.payload = "test" + + expected = Response() + expected.type = defines.Types["ACK"] + expected._mid = self.current_mid + expected.code = defines.Codes.CHANGED.number + expected.token = None + expected.payload = None + expected.location_path = "/obs" + + self.current_mid += 1 + self._test_with_client([(req, expected)]) + +if __name__ == '__main__': + unittest.main() diff --git a/plugtest_coapserver.py b/plugtest_coapserver.py new file mode 100644 index 0000000..2b91918 --- /dev/null +++ b/plugtest_coapserver.py @@ -0,0 +1,75 @@ +import getopt +import logging +import sys +from plugtest_resources import TestResource, SeparateResource, ObservableResource, LargeResource, LargeUpdateResource, \ + LongResource +from coapthon.server.coap import CoAP + +__author__ = 'Giacomo Tanganelli' + + +class CoAPServerPlugTest(CoAP): + def __init__(self, host, port, multicast=False, starting_mid=None): + CoAP.__init__(self, (host, port), multicast, starting_mid) + + # create logger + logger = logging.getLogger(__name__) + logger.setLevel(logging.DEBUG) + + # create console handler and set level to debug + ch = logging.StreamHandler() + ch.setLevel(logging.DEBUG) + + # create formatter + formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') + + # add formatter to ch + ch.setFormatter(formatter) + + # add ch to logger + logger.addHandler(ch) + + self.add_resource('test/', TestResource()) + self.add_resource('separate/', SeparateResource()) + self.add_resource('seg1/', TestResource()) + self.add_resource('seg1/seg2/', TestResource()) + self.add_resource('seg1/seg2/seg3/', TestResource()) + self.add_resource('query/', TestResource()) + self.add_resource("obs/", ObservableResource(coap_server=self)) + self.add_resource("large/", LargeResource(coap_server=self)) + self.add_resource("large-update/", LargeUpdateResource(coap_server=self)) + self.add_resource('long/', LongResource()) + + +def usage(): # pragma: no cover + print("plugtest_coapserver.py -i -p ") + + +def main(argv): # pragma: no cover + ip = "127.0.0.1" + port = 5683 + try: + opts, args = getopt.getopt(argv, "hi:p:", ["ip=", "port="]) + except getopt.GetoptError: + usage() + sys.exit(2) + for opt, arg in opts: + if opt == '-h': + usage() + sys.exit() + elif opt in ("-i", "--ip"): + ip = arg + elif opt in ("-p", "--port"): + port = int(arg) + + server = CoAPServerPlugTest(ip, port) + try: + server.listen(10) + except KeyboardInterrupt: + print("Server Shutdown") + server.close() + print("Exiting...") + + +if __name__ == "__main__": + main(sys.argv[1:]) diff --git a/plugtest_resources.py b/plugtest_resources.py new file mode 100644 index 0000000..ca4ccd3 --- /dev/null +++ b/plugtest_resources.py @@ -0,0 +1,150 @@ +# coding=utf-8 +import logging +import threading +import time + +import datetime + +from coapthon import defines +from coapthon.resources.resource import Resource + +__author__ = 'Giacomo Tanganelli' + + +logger = logging.getLogger(__name__) + + +class TestResource(Resource): + def __init__(self, name="TestResource", coap_server=None): + super(TestResource, self).__init__(name, coap_server, visible=True, observable=False, allow_children=True) + self.payload = "Test Resource" + self.resource_type = "Type1" + self.maximum_size_estimated = len(self.payload) + + def render_GET(self, request): + return self + + def render_PUT(self, request): + for option in request.options: + if option.number == defines.OptionRegistry.CONTENT_TYPE.number: + self.payload = (option.value, request.payload) + return self + self.payload = request.payload + return self + + def render_POST(self, request): + res = TestResource() + res.location_query = request.uri_query + for option in request.options: + if option.number == defines.OptionRegistry.CONTENT_TYPE.number: + res.payload = {option.value: request.payload} + return res + + res.payload = request.payload + return res + + def render_DELETE(self, request): + return True + + +class SeparateResource(Resource): + + def __init__(self, name="Separate", coap_server=None): + super(SeparateResource, self).__init__(name, coap_server, visible=True, observable=False, allow_children=False) + self.payload = "Separate Resource" + self.interface_type = "separate" + self.add_content_type("text/plain") + + def render_GET(self, request): + return self, self.render_GET_separate + + def render_GET_separate(self, request): + time.sleep(5) + return self + + +class ObservableResource(Resource): + + def __init__(self, name="Obs", coap_server=None): + super(ObservableResource, self).__init__(name, coap_server, visible=True, observable=True, allow_children=False) + self.payload = "Observable Resource" + self.period = 5 + self.update(True) + + def render_GET(self, request): + return self + + def render_POST(self, request): + self.payload = request.payload + return self + + def update(self, first=False): + self.payload = "Observable Resource" + if not self._coap_server.stopped.isSet(): + + timer = threading.Timer(self.period, self.update) + timer.setDaemon(True) + timer.start() + + if not first and self._coap_server is not None: + logger.debug("Periodic Update") + self._coap_server.notify(self) + self.observe_count += 1 + + +class LargeResource(Resource): + + def __init__(self, name="Large", coap_server=None): + super(LargeResource, self).__init__(name, coap_server, visible=True, observable=False, allow_children=False) + # 2000 bytes + self.payload = """"Me sabbee plenty"—grunted Queequeg, puffing away at his pipe and sitting up in bed. +"You gettee in," he added, motioning to me with his tomahawk, and throwing the clothes to one side. He really did this +in not only a civil but a really kind and charitable way. I stood looking at him a moment. For all his tattooings +he was on the whole a clean, comely looking cannibal. What's all this fuss I have been making about, thought I to +myself—the man's a human being just as I am: he has just as much reason to fear me, as I have to be afraid of him. +Better sleep with a sober cannibal than a drunken Christian. +"Landlord," said I, "tell him to stash his tomahawk there, or pipe, or whatever you call it; tell him to stop smoking, +in short, and I will turn in with him. But I don't fancy having a man smoking in bed with me. It's dangerous. Besides, +I ain't insured." +This being told to Queequeg, he at once complied, and again politely motioned me to get into bed—rolling over to one +side as much as to say—"I won't touch a leg of ye." +"Good night, landlord," said I, "you may go." +I turned in, and never slept better in my life. +Upon waking next morning about daylight, I found Queequeg's arm thrown over me in the most loving and affectionate +manner. You had almost thought I had been his wife. The counterpane was of patchwork, full of odd little +parti-coloured squares and triangles; and this arm of his tattooed all over with an interminable Cretan labyrinth +of a figure, no two parts of which were of one precise shade—owing I suppose to his keeping his arm at sea +unmethodically in sun and shade, his shirt sleeves irregularly rolled up at various times—this same arm of his, +I say, looked for all the world like a strip of that same patchwork quilt. Indeed, partly lying on it as the arm did + when I first awoke, I could hardly tell it from the quilt, they so blended their hues together; and it was only by + the sense of weight and pressure that I could tell that Queequeg was hugging""" + + def render_GET(self, request): + return self + + +class LargeUpdateResource(Resource): + + def __init__(self, name="Large", coap_server=None): + super(LargeUpdateResource, self).__init__(name, coap_server, visible=True, observable=False, + allow_children=False) + self.payload = "" + + def render_GET(self, request): + return self + + def render_PUT(self, request): + self.payload = request.payload + return self + + +class LongResource(Resource): + + def __init__(self, name="Large", coap_server=None): + super(LongResource, self).__init__(name, coap_server, visible=True, observable=False, + allow_children=False) + self.payload = "" + + def render_GET(self, request): + time.sleep(5) + return self diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..5c59f63 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +Sphinx==1.2.2 +cachetools==2.0.0 + diff --git a/reverse_proxy_mapping.xml b/reverse_proxy_mapping.xml new file mode 100644 index 0000000..1b5b528 --- /dev/null +++ b/reverse_proxy_mapping.xml @@ -0,0 +1,4 @@ + + + 127.0.0.1:5684 + \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..6c31ae8 --- /dev/null +++ b/setup.py @@ -0,0 +1,15 @@ +from distutils.core import setup + +setup( + name='CoAPthon', + version='4.0.2', + packages=['coapthon', 'coapthon.caching', 'coapthon.layers', 'coapthon.client', 'coapthon.server', 'coapthon.messages', + 'coapthon.forward_proxy', 'coapthon.resources', 'coapthon.reverse_proxy'], + url='https://github.com/Tanganelli/CoAPthon', + license='MIT License', + author='Giacomo Tanganelli', + author_email='giacomo.tanganelli@for.unipi.it', + description='CoAPthon is a python library to the CoAP protocol. ', + scripts=['coapserver.py', 'coapclient.py', 'exampleresources.py', 'coapforwardproxy.py', 'coapreverseproxy.py'], + requires=['sphinx', 'cachetools'] +)