You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
CoAP/coapthon/layers/blocklayer.py

326 lines
14 KiB
Python

import logging
from coapthon import defines
from coapthon import utils
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 = {} # type: dict[hash, BlockItem]
self._block2_sent = {} # type: dict[hash, BlockItem]
self._block1_receive = {} # type: dict[hash, BlockItem]
self._block2_receive = {} # type: dict[hash, BlockItem]
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 = utils.str_append_hash(host, port, 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 = num * size
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 = utils.str_append_hash(host, port, transaction.request.token)
num, m, size = transaction.request.block1
if transaction.request.size1 is not None:
# What to do if the size1 is larger than the maximum resource size or the maxium server buffer
pass
if key_token in self._block1_receive:
# n-th block
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 = utils.str_append_hash(host, port, 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:
item.m = 0
else:
item.m = 1
request.block1 = (item.num, item.m, item.size)
# The original request already has this option set
# request.size1 = len(item.payload)
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 = utils.str_append_hash(host, port, 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)
# correct m
m = 0 if ((num * size) + size) > len(transaction.response.payload) else 1
# add size2 if requested or if payload is bigger than one datagram
del transaction.response.size2
if (transaction.request.size2 is not None and transaction.request.size2 == 0) or \
(transaction.response.payload is not None and len(transaction.response.payload) > defines.MAX_PAYLOAD):
transaction.response.size2 = len(transaction.response.payload)
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 = utils.str_append_hash(host, port, request.token)
if request.block1:
num, m, size = request.block1
else:
num = 0
m = 1
size = defines.MAX_PAYLOAD
# correct m
m = 0 if ((num * size) + size) > len(request.payload) else 1
del request.size1
request.size1 = len(request.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 = utils.str_append_hash(host, port, 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