Porting to Python3

This commit is contained in:
giacomo.tanganelli@for.unipi.it 2018-01-23 11:31:46 +01:00
parent 4ed42cfb01
commit 4db21cd7e7
76 changed files with 19388 additions and 0 deletions

19
.coveragerc Normal file
View file

@ -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__.:

19
.travis.yml Normal file
View file

@ -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

493
cache_test.py Normal file
View file

@ -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()

162
coapclient.py Normal file
View file

@ -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()

48
coapforwardproxy.py Normal file
View file

@ -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 <ip address> -p <port>")
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:])

107
coapping.py Normal file
View file

@ -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

52
coapreverseproxy.py Normal file
View file

@ -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 <ip address> -p <port> -f <xml_file>")
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:])

67
coapserver.py Normal file
View file

@ -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 <ip address> -p <port>")
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:])

1
coapthon/__init__.py Normal file
View file

@ -0,0 +1 @@
__author__ = 'Giacomo Tanganelli'

View file

@ -0,0 +1 @@
author = "Emilio"

246
coapthon/caching/cache.py Normal file
View file

@ -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)

View file

@ -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)

View file

@ -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

View file

@ -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())
])))

View file

@ -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())
])))

View file

@ -0,0 +1 @@
__author__ = 'Giacomo Tanganelli'

318
coapthon/client/coap.py Normal file
View file

@ -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)

317
coapthon/client/coap.py.bak Normal file
View file

@ -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)

View file

@ -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

View file

@ -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

300
coapthon/defines.py Normal file
View file

@ -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"
}

View file

@ -0,0 +1 @@
__author__ = 'Giacomo Tanganelli'

View file

@ -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)

View file

@ -0,0 +1 @@

View file

@ -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 <scheme>://<netloc>/<path>;<params>?<query>#<fragment>
: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 = "<html><body><h1>", coap_response.payload, "</h1></body></html>"
self.wfile.write("".join(body))
else:
self.wfile.write("<html><body><h1>None</h1></body></html>")
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()

View file

@ -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 <scheme>://<netloc>/<path>;<params>?<query>#<fragment>
: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 = "<html><body><h1>", coap_response.payload, "</h1></body></html>"
self.wfile.write("".join(body))
else:
self.wfile.write("<html><body><h1>None</h1></body></html>")
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()

View file

@ -0,0 +1 @@
__author__ = 'Giacomo Tanganelli'

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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")

View file

@ -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")

View file

@ -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

View file

@ -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

View file

@ -0,0 +1 @@
__author__ = 'Giacomo Tanganelli'

View file

@ -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

View file

@ -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

137
coapthon/messages/option.py Normal file
View file

@ -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__

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -0,0 +1 @@
__author__ = 'Giacomo Tanganelli'

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -0,0 +1 @@
__author__ = 'Giacomo Tanganelli'

View file

@ -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)

408
coapthon/serializer.py Normal file
View file

@ -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

397
coapthon/serializer.py.bak Normal file
View file

@ -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

View file

@ -0,0 +1 @@
__author__ = 'Giacomo Tanganelli'

422
coapthon/server/coap.py Normal file
View file

@ -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)

421
coapthon/server/coap.py.bak Normal file
View file

@ -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)

156
coapthon/transaction.py Normal file
View file

@ -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

188
coapthon/utils.py Normal file
View file

@ -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]

188
coapthon/utils.py.bak Normal file
View file

@ -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]

141
collectclient.py Normal file
View file

@ -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()

337
collectserver.py Normal file
View file

@ -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 <ip address> -p <port>")
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:])

1576
coverage_test.py Normal file

File diff suppressed because it is too large Load diff

164
coverage_testIPv6.py Normal file
View file

@ -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()

214
coverage_test_advanced.py Normal file
View file

@ -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()

165
coverage_test_multicast.py Normal file
View file

@ -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()

View file

@ -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()

1421
coverage_test_proxy.py Normal file

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

74
dimmerserver.py Normal file
View file

@ -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()

281
exampleresources.py Normal file
View file

@ -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"], "<value>"+str(self.value)+"</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"], "<value>"+str(self.value)+"</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

22
logging.conf Normal file
View file

@ -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=

7
lorem_impsum.txt Normal file
View file

@ -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.

889
plugtest.py Normal file
View file

@ -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 = """</large>;</large-update>;</long>;</obs>;obs,</query>;rt="Type1";sz="13",</seg1>;rt="Type1";sz="13",</seg1/seg2>;rt="Type1";sz="13",</seg1/seg2/seg3>;rt="Type1";sz="13",</separate>;ct=0;if="separate",</test>;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 = """</query>;rt="Type1";sz="13",</seg1>;rt="Type1";sz="13",</seg1/seg2>;rt="Type1";sz="13",</seg1/seg2/seg3>;rt="Type1";sz="13",</test>;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 = "<value>test</value>"
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 = "<value>test</value>"
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 = "<value>test</value>"
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 = "<value>test</value>"
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 = "<value>test</value>"
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
myselfthe 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 bedrolling 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 shadeowing I suppose to his keeping his arm at sea
unmethodically in sun and shade, his shirt sleeves irregularly rolled up at various timesthis 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
myselfthe 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 bedrolling 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 shadeowing I suppose to his keeping his arm at sea
unmethodically in sun and shade, his shirt sleeves irregularly rolled up at various timesthis 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 = "<value>test</value>"
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()

75
plugtest_coapserver.py Normal file
View file

@ -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 <ip address> -p <port>")
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:])

150
plugtest_resources.py Normal file
View file

@ -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
myselfthe 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 bedrolling 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 shadeowing I suppose to his keeping his arm at sea
unmethodically in sun and shade, his shirt sleeves irregularly rolled up at various timesthis 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

3
requirements.txt Normal file
View file

@ -0,0 +1,3 @@
Sphinx==1.2.2
cachetools==2.0.0

View file

@ -0,0 +1,4 @@
<servers>
<!--<server name="Server1">[::1]:5684</server>-->
<server name="Server1">127.0.0.1:5684</server>
</servers>

15
setup.py Normal file
View file

@ -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']
)