321 lines
13 KiB
Python
321 lines
13 KiB
Python
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 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 = 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:
|
|
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 = 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)
|
|
|
|
# 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 = 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
|
|
# 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 = 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
|
|
|