diff --git a/bin/test_client.py b/bin/test_client.py index 0164861..e8cb374 100644 --- a/bin/test_client.py +++ b/bin/test_client.py @@ -26,7 +26,7 @@ try: # retrieve value of 'test' resource - p = c.GET('coap://[{0}]/test'.format(SERVER_IP), + p,m = c.GET('coap://[{0}]/test'.format(SERVER_IP), confirmable=True, options=[objectSecurity]) diff --git a/bin/test_server.py b/bin/test_server.py index 9ba5f76..c07c253 100644 --- a/bin/test_server.py +++ b/bin/test_server.py @@ -20,7 +20,7 @@ def __init__(self): path = 'test', ) - def GET(self,options=[]): + def GET(self,options=[],metaData={}): print 'GET received' diff --git a/bin/test_simulation.py b/bin/test_simulation.py index 8d37dd0..e3d5535 100644 --- a/bin/test_simulation.py +++ b/bin/test_simulation.py @@ -16,17 +16,17 @@ c.respTimeout = 2 # sec # get status of LED -p = c.GET('coap://[{0}]/l'.format(MOTE_IP),) +p,m = c.GET('coap://[{0}]/l'.format(MOTE_IP),) print chr(p[0]) # toggle debug LED -p = c.PUT( +p,m = c.PUT( 'coap://[{0}]/l'.format(MOTE_IP), payload = [ord('2')], ) # read status of debug LED -p = c.GET('coap://[{0}]/l'.format(MOTE_IP)) +p,m = c.GET('coap://[{0}]/l'.format(MOTE_IP)) print chr(p[0]) # close diff --git a/coap/coap.py b/coap/coap.py index 57884ee..e417299 100644 --- a/coap/coap.py +++ b/coap/coap.py @@ -23,10 +23,11 @@ def emit(self, record): import coapTransmitter from socketUdpDispatcher import socketUdpDispatcher from socketUdpReal import socketUdpReal +from socketUdp import socketUdp as socketUdpAbstract class coap(object): - def __init__(self,ipAddress='',udpPort=d.DEFAULT_UDP_PORT,testing=False,receiveCallback=None): + def __init__(self,ipAddress='',udpPort=d.DEFAULT_UDP_PORT,testing=False,socketUdp=None): # store params self.ipAddress = ipAddress @@ -43,23 +44,19 @@ def __init__(self,ipAddress='',udpPort=d.DEFAULT_UDP_PORT,testing=False,receiveC self.respTimeout = d.DFLT_RESPONSE_TIMEOUT self.maxRetransmit = d.DFLT_MAX_RETRANSMIT self.secContextHandler = None - if receiveCallback: - callback = receiveCallback - else: - callback = self._receive - if testing: - self.socketUdp = socketUdpDispatcher( - ipAddress = self.ipAddress, - udpPort = self.udpPort, - callback = callback, - ) + + if socketUdp is not None: + socketClass = socketUdp + elif testing: + socketClass = socketUdpDispatcher else: - self.socketUdp = socketUdpReal( - ipAddress = self.ipAddress, - udpPort = self.udpPort, - callback = callback, - ) + socketClass = socketUdpReal + self.socketUdp = socketClass( + ipAddress = self.ipAddress, + udpPort = self.udpPort, + callback = self._receive, + ) #======================== public ========================================== def close(self): @@ -76,7 +73,7 @@ def GET(self,uri,confirmable=True,options=[]): options = options, ) log.debug('response: {0}'.format(response)) - return response['payload'] + return response['payload'], response['metaData'] def PUT(self,uri,confirmable=True,options=[],payload=None): response = self._transmit( @@ -87,7 +84,7 @@ def PUT(self,uri,confirmable=True,options=[],payload=None): payload = payload ) log.debug('response: {0}'.format(response)) - return response['payload'] + return response['payload'], response['metaData'] def POST(self,uri,confirmable=True,options=[],payload=None): response = self._transmit( @@ -98,7 +95,7 @@ def POST(self,uri,confirmable=True,options=[],payload=None): payload = payload ) log.debug('response: {0}'.format(response)) - return response['payload'] + return response['payload'], response['metaData'] def DELETE(self,uri,confirmable=True,options=[]): self._transmit( @@ -239,10 +236,20 @@ def _receive(self,timestamp,sender,rawbytes): srcPort = sender[1] + # meta data useful in testing mode + metaData = {} + metaData['srcIP'] = srcIp + metaData['srcPort'] = srcPort + i = 0 + for var in sender[2]: + metaData['generic_{0}'.format(i)] = var + i += 1 + # parse messages try: message = m.parseMessage(rawbytes) options = message['options'] + message['metaData'] = metaData except e.messageFormatError as err: log.warning('malformed message {0}: {1}'.format(u.formatBuf(rawbytes),str(err))) return @@ -326,21 +333,25 @@ def _receive(self,timestamp,sender,rawbytes): try: if message['code']==d.METHOD_GET and d.METHOD_GET in authorizedMethods: (respCode,respOptions,respPayload) = resource.GET( - options=options + options=options, + metaData=metaData, ) elif message['code']==d.METHOD_POST and d.METHOD_POST in authorizedMethods: (respCode,respOptions,respPayload) = resource.POST( options=options, - payload=payload + payload=payload, + metaData=metaData, ) elif message['code']==d.METHOD_PUT and d.METHOD_PUT in authorizedMethods: (respCode,respOptions,respPayload) = resource.PUT( options=options, - payload=payload + payload=payload, + metaData=metaData, ) elif message['code']==d.METHOD_DELETE and d.METHOD_DELETE in authorizedMethods: (respCode,respOptions,respPayload) = resource.DELETE( - options=options + options=options, + metaData=metaData, ) elif message['code'] not in d.METHOD_ALL: raise SystemError('unexpected code {0}'.format(message['code'])) @@ -369,11 +380,20 @@ def _receive(self,timestamp,sender,rawbytes): objectSecurity = o.ObjectSecurity(context=foundContext) respOptions += [objectSecurity] - # if Stateless-Proxy option was present in the request echo it + # process special options for option in options: + # if Stateless - Proxy option was present in the request echo it if isinstance(option, o.StatelessProxy): respOptions += [option] - break + # if No Response option was present in the request, don't send the response + if isinstance(option, o.NoResponse): + if option.getPayloadBytes() == [d.DFLT_OPTION_NORESPONSE_SUPRESS_ALL]: + # exit without returning any response + log.info("Suppressing a response due to the {0} option in the request.".format(option)) + return + else: + # selective suppression not implemented for now + raise NotImplementedError() # build response packets and pass partialIV from the request for OSCOAP's processing response = m.buildMessage( @@ -406,7 +426,7 @@ def _receive(self,timestamp,sender,rawbytes): for (k,v) in self.transmitters.items(): # try matching if ( - msgkey[0]==k[0] and + u.ipv6AddrString2Bytes(msgkey[0])==u.ipv6AddrString2Bytes(k[0]) and msgkey[1]==k[1] and ( msgkey[2]==k[2] or diff --git a/coap/coapDefines.py b/coap/coapDefines.py index c2483ae..3a1f68f 100644 --- a/coap/coapDefines.py +++ b/coap/coapDefines.py @@ -17,6 +17,9 @@ DFLT_EXCHANGE_LIFETIME = 248 # lifetime of a message ID DFLT_RESPONSE_TIMEOUT = 60 # delay for app-level response +# Option value helpers +DFLT_OPTION_NORESPONSE_SUPRESS_ALL = 26 # RFC7967 value to supress all responses + # CoAP Message Types TYPE_CON = 0 TYPE_NON = 1 @@ -115,6 +118,7 @@ OPTION_NUM_PROXYSCHEME = 39 OPTION_NUM_OBJECT_SECURITY = 21 # plugtest value OPTION_NUM_STATELESSPROXY = 40 # experimental value +OPTION_NUM_NORESPONSE = 258 # RFC7967 OPTION_NUM_ALL = [ OPTION_NUM_IFMATCH, OPTION_NUM_URIHOST, @@ -134,6 +138,7 @@ OPTION_NUM_PROXYSCHEME, OPTION_NUM_OBJECT_SECURITY, OPTION_NUM_STATELESSPROXY, + OPTION_NUM_NORESPONSE, ] # CoAP Content-Format Registry diff --git a/coap/coapException.py b/coap/coapException.py index 997d631..0494782 100644 --- a/coap/coapException.py +++ b/coap/coapException.py @@ -25,6 +25,11 @@ def __str__(self): class coapDelayedResponse(coapException): pass +#============================ no response needed ============================== + +class coapNoResponseExpected(coapException): + pass + #============================ timeout ========================================= class coapTimeout(coapException): diff --git a/coap/coapOption.py b/coap/coapOption.py index 802390a..a5860af 100644 --- a/coap/coapOption.py +++ b/coap/coapOption.py @@ -271,6 +271,25 @@ def __repr__(self): def getPayloadBytes(self): return self.opaqueValue + +# === OPTION_NUM_NORESPONSE +class NoResponse(coapOption): + def __init__(self, value=[d.DFLT_OPTION_NORESPONSE_SUPRESS_ALL]): + # initialize parent + coapOption.__init__(self, d.OPTION_NUM_NORESPONSE, d.OSCOAP_CLASS_E) + + # store params + if len(value) != 1: + raise e.messageFormatError + + self.supressionValue = value + + def __repr__(self): + return 'NoResponse(value={0})'.format(self.supressionValue) + + def getPayloadBytes(self): + return self.supressionValue + #============================ functions ======================================= def parseOption(message,previousOptionNumber): @@ -377,6 +396,8 @@ def parseOption(message,previousOptionNumber): option = ProxyScheme(scheme=''.join([chr(b) for b in optionValue])) elif optionNumber==d.OPTION_NUM_STATELESSPROXY: option = StatelessProxy(value=optionValue) + elif optionNumber==d.OPTION_NUM_NORESPONSE: + option = NoResponse(value=optionValue) else: raise NotImplementedError('option {0} not implemented'.format(optionNumber)) diff --git a/coap/coapResource.py b/coap/coapResource.py index 3fbc9ea..a1d7c22 100644 --- a/coap/coapResource.py +++ b/coap/coapResource.py @@ -23,16 +23,16 @@ def __init__(self,path): #======================== abstract methods ================================ - def GET(self,options=[]): + def GET(self,options=[],metaData={}): raise e.coapRcMethodNotAllowed() - def PUT(self,options=[],payload=None): + def PUT(self,options=[],payload=None,metaData={}): raise e.coapRcMethodNotAllowed() - def POST(self,options=[],payload=None): + def POST(self,options=[],payload=None,metaData={}): raise e.coapRcMethodNotAllowed() - def DELETE(self,options=[]): + def DELETE(self,options=[],metaData={}): raise e.coapRcMethodNotAllowed() #======================== public ========================================== diff --git a/coap/coapTransmitter.py b/coap/coapTransmitter.py index 4586924..0d62811 100644 --- a/coap/coapTransmitter.py +++ b/coap/coapTransmitter.py @@ -14,6 +14,7 @@ def emit(self, record): import coapException as e import coapUtils as u import coapMessage as m +import coapOption as o class coapTransmitter(threading.Thread): ''' @@ -36,6 +37,7 @@ class coapTransmitter(threading.Thread): STATE_WAITFOREXPIRATIONMID = 'WAITFOREXPIRATIONMID' STATE_WAITFORRESP = 'WAITFORRESP' STATE_RESPRX = 'RESPRX' + STATE_NORESPEXPECTED = 'NORESPEXPECTED' STATE_TXACK = 'TXACK' STATE_ALL = [ STATE_INIT, @@ -133,6 +135,7 @@ def __init__(self,sendFunc,srcIp,srcPort,destIp,destPort,confirmable,messageId,c self.STATE_WAITFORRESP: self._action_WAITFORRESP, self.STATE_RESPRX: self._action_RESPRX, self.STATE_TXACK: self._action_TXACK, + self.STATE_NORESPEXPECTED: self._action_NORESPEXPECTED, } # initialize parent @@ -197,7 +200,7 @@ def getState(self): return self.state def receiveMessage(self, timestamp, srcIp, srcPort, message): - assert srcIp==self.destIp + assert u.ipv6AddrString2Bytes(srcIp)==u.ipv6AddrString2Bytes(self.destIp) assert srcPort==self.destPort assert (message['token']==self.token) or (message['messageId']==self.messageId) @@ -331,8 +334,21 @@ def _action_TXNON(self): msg = message, ) - # update FSM state - self._setState(self.STATE_WAITFORRESP) + noResponse = False + for option in self.options: + if isinstance(option, o.NoResponse): + if option.getPayloadBytes() == [d.DFLT_OPTION_NORESPONSE_SUPRESS_ALL]: + # do not expect a response + noResponse = True + else: + # selective suppression not implemented for now + raise NotImplementedError() + # in case no response is expected, do not hang waiting for the timeout + if not noResponse: + # update FSM state + self._setState(self.STATE_WAITFORRESP) + else: + self._setState(self.STATE_NORESPEXPECTED) # kick FSM self._kickFsm() @@ -473,6 +489,18 @@ def _action_RESPRX(self): # kick FSM self._kickFsm() + def _action_NORESPEXPECTED(self): + + # log + log.debug('_action_NORESPEXPECTED()') + + # successful end of FSM + with self.dataLock: + self.coapError = e.coapNoResponseExpected('No Response is expected for this request') + + # kick FSM + self._kickFsm() + def _action_TXACK(self): # log diff --git a/coap/socketUdpDispatcher.py b/coap/socketUdpDispatcher.py index cf210a5..23285a1 100644 --- a/coap/socketUdpDispatcher.py +++ b/coap/socketUdpDispatcher.py @@ -44,7 +44,7 @@ def sendUdp(self,destIp,destPort,msg): # send over dispatcher dispatcher.send( signal = (destIp,destPort), - sender = (self.ipAddress,self.udpPort), + sender = (self.ipAddress,self.udpPort, ()), data = msg ) diff --git a/coap/socketUdpReal.py b/coap/socketUdpReal.py index d24f3bd..107f32a 100644 --- a/coap/socketUdpReal.py +++ b/coap/socketUdpReal.py @@ -92,7 +92,7 @@ def run(self): continue timestamp = time.time() - source = (conn[0],conn[1]) + source = (conn[0],conn[1],()) data = [ord(b) for b in raw] log.debug("got {2} from {1} at {0}".format(timestamp,source,data)) diff --git a/tests/conftest.py b/tests/conftest.py index c8215a5..756420f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -143,7 +143,7 @@ def __init__(self): #======================== parent methods ================================== - def GET(self,options=[]): + def GET(self,options=[],metaData={}): log.debug('dummyResource GET') respCode = d.COAP_RC_2_05_CONTENT diff --git a/tests/func/test_BADREQUEST.py b/tests/func/test_BADREQUEST.py index d48cee2..66dfd8a 100644 --- a/tests/func/test_BADREQUEST.py +++ b/tests/func/test_BADREQUEST.py @@ -43,7 +43,7 @@ def test_BADREQUEST(logFixture, snoopyDispatcher, twoEndPoints, confirmableFixtu clientOptions = [o.ObjectSecurity(context=clientContext)] with pytest.raises(e.coapRcBadRequest): - reply = coap2.GET( + reply, meta = coap2.GET( uri='coap://[{0}]:{1}/{2}/'.format(IPADDRESS1, d.DEFAULT_UDP_PORT, RESOURCE), confirmable=confirmableFixture, options=clientOptions diff --git a/tests/func/test_INTERNALSERVERERROR.py b/tests/func/test_INTERNALSERVERERROR.py index 91d931b..d423cb9 100644 --- a/tests/func/test_INTERNALSERVERERROR.py +++ b/tests/func/test_INTERNALSERVERERROR.py @@ -35,7 +35,7 @@ def __init__(self): #======================== parent methods ================================== - def GET(self,options=[]): + def GET(self,options=[],metaData={}): log.debug('buggyResource GET') # raise some exception @@ -71,7 +71,7 @@ def test_GET(logFixture,snoopyDispatcher,twoEndPoints): # have coap2 do a get with pytest.raises(e.coapRcInternalServerError): - reply = coap2.GET( + reply, meta = coap2.GET( uri = 'coap://[{0}]:{1}/{2}/'.format(IPADDRESS1,d.DEFAULT_UDP_PORT,'buggy'), confirmable = True, options=clientOptions diff --git a/tests/func/test_METHODNOTALLOWED.py b/tests/func/test_METHODNOTALLOWED.py index d038b4d..00b92d3 100644 --- a/tests/func/test_METHODNOTALLOWED.py +++ b/tests/func/test_METHODNOTALLOWED.py @@ -40,7 +40,7 @@ def test_METHODNOTALLOWED(logFixture,snoopyDispatcher,twoEndPoints,confirmableFi # have coap2 do a post with pytest.raises(e.coapRcMethodNotAllowed): - reply = coap2.POST( + reply, meta = coap2.POST( uri = 'coap://[{0}]:{1}/{2}/'.format(IPADDRESS1,d.DEFAULT_UDP_PORT,RESOURCE), confirmable = confirmableFixture, options=options diff --git a/tests/func/test_NORESPONSE.py b/tests/func/test_NORESPONSE.py new file mode 100644 index 0000000..f88af5f --- /dev/null +++ b/tests/func/test_NORESPONSE.py @@ -0,0 +1,48 @@ +import logging +import testUtils as utils + +import time +import threading + +import pytest + +from conftest import IPADDRESS1, \ + RESOURCE, \ + DUMMYVAL, \ + OSCOAPMASTERSECRET, \ + OSCOAPSERVERID, \ + OSCOAPCLIENTID +from coap import coapDefines as d, \ + coapException as e, \ + coapOption as o, \ + coapObjectSecurity as oscoap + +# ============================ logging =============================== + +log = logging.getLogger(utils.getMyLoggerName()) +log.addHandler(utils.NullHandler()) + + +# ============================ tests =========================================== + +def test_GET(logFixture, snoopyDispatcher, twoEndPoints): + (coap1, coap2, securityEnabled) = twoEndPoints + + options = [] + if securityEnabled: + context = oscoap.SecurityContext(masterSecret=OSCOAPMASTERSECRET, + senderID=OSCOAPSERVERID, + recipientID=OSCOAPCLIENTID) + + options = [o.ObjectSecurity(context=context)] + + options += [o.NoResponse()] + + # have coap2 do a get + with pytest.raises(e.coapNoResponseExpected): + reply, meta = coap2.GET( + uri='coap://[{0}]:{1}/{2}/'.format(IPADDRESS1, d.DEFAULT_UDP_PORT, RESOURCE), + confirmable=False, + options=options, + ) + diff --git a/tests/func/test_NOTFOUND.py b/tests/func/test_NOTFOUND.py index 09c9567..6437e71 100644 --- a/tests/func/test_NOTFOUND.py +++ b/tests/func/test_NOTFOUND.py @@ -41,7 +41,7 @@ def test_NOTFOUND(logFixture,snoopyDispatcher,twoEndPoints,confirmableFixture): # have coap2 do a get with pytest.raises(e.coapRcNotFound): - reply = coap2.GET( + reply, meta = coap2.GET( uri = 'coap://[{0}]:{1}/{2}/'.format(IPADDRESS1,d.DEFAULT_UDP_PORT,RESOURCE_INVALID), confirmable = confirmableFixture, options=options, diff --git a/tests/func/test_UNAUTHORIZED.py b/tests/func/test_UNAUTHORIZED.py index 716a8fd..8265309 100644 --- a/tests/func/test_UNAUTHORIZED.py +++ b/tests/func/test_UNAUTHORIZED.py @@ -61,7 +61,7 @@ def test_UNAUTHORIZED_2(logFixture, snoopyDispatcher, twoEndPoints, confirmableF clientOptions = [o.ObjectSecurity(context=clientContext)] with pytest.raises(e.coapRcUnauthorized): - reply = coap2.GET( + reply, meta = coap2.GET( uri='coap://[{0}]:{1}/{2}/'.format(IPADDRESS1, d.DEFAULT_UDP_PORT, RESOURCE), confirmable=confirmableFixture, options=clientOptions diff --git a/tests/func/test_multiple_CON.py b/tests/func/test_multiple_CON.py index 66da71f..01ee56d 100644 --- a/tests/func/test_multiple_CON.py +++ b/tests/func/test_multiple_CON.py @@ -36,7 +36,7 @@ def test_GET(logFixture,snoopyDispatcher,twoEndPoints): # have coap2 do a get for _ in range(20): - reply = coap2.GET( + reply, meta = coap2.GET( uri = 'coap://[{0}]:{1}/{2}/'.format(IPADDRESS1,d.DEFAULT_UDP_PORT,RESOURCE), confirmable = False, options=options diff --git a/tests/func/test_multiple_NON.py b/tests/func/test_multiple_NON.py index 8c5a469..3f12dea 100644 --- a/tests/func/test_multiple_NON.py +++ b/tests/func/test_multiple_NON.py @@ -35,7 +35,7 @@ def test_GET(logFixture,snoopyDispatcher,twoEndPoints): # have coap2 do a get for _ in range(20): - reply = coap2.GET( + reply, meta = coap2.GET( uri = 'coap://[{0}]:{1}/{2}/'.format(IPADDRESS1,d.DEFAULT_UDP_PORT,RESOURCE), confirmable = False, options = options diff --git a/tests/func/test_single_CON.py b/tests/func/test_single_CON.py index 4e49492..46ce885 100644 --- a/tests/func/test_single_CON.py +++ b/tests/func/test_single_CON.py @@ -33,7 +33,7 @@ def test_GET(logFixture,snoopyDispatcher,twoEndPoints): options = [o.ObjectSecurity(context=context)] # have coap2 do a get - reply = coap2.GET( + reply, meta = coap2.GET( uri='coap://[{0}]:{1}/{2}/'.format(IPADDRESS1, d.DEFAULT_UDP_PORT, RESOURCE), confirmable=False, options=options diff --git a/tests/func/test_single_NON.py b/tests/func/test_single_NON.py index aeb5e32..fac0f6f 100644 --- a/tests/func/test_single_NON.py +++ b/tests/func/test_single_NON.py @@ -34,7 +34,7 @@ def test_GET(logFixture,snoopyDispatcher,twoEndPoints): options = [o.ObjectSecurity(context=context)] # have coap2 do a get - reply = coap2.GET( + reply, meta = coap2.GET( uri = 'coap://[{0}]:{1}/{2}/'.format(IPADDRESS1,d.DEFAULT_UDP_PORT,RESOURCE), confirmable = False, options=options, diff --git a/tests/func/test_timeout_CON.py b/tests/func/test_timeout_CON.py index 0b6264b..9937f46 100644 --- a/tests/func/test_timeout_CON.py +++ b/tests/func/test_timeout_CON.py @@ -46,7 +46,7 @@ def test_GET(logFixture,snoopyDispatcher,twoEndPoints): # have coap2 do a get with pytest.raises(e.coapTimeout): - reply = coap2.GET( + reply, meta = coap2.GET( uri = 'coap://[{0}]:{1}/{2}/'.format(IPADDRESS_INVALID,d.DEFAULT_UDP_PORT,RESOURCE), confirmable = True, options=options, diff --git a/tests/func/test_timeout_NON.py b/tests/func/test_timeout_NON.py index be6cb08..76bd148 100644 --- a/tests/func/test_timeout_NON.py +++ b/tests/func/test_timeout_NON.py @@ -46,7 +46,7 @@ def test_GET(logFixture,snoopyDispatcher,twoEndPoints): # have coap2 do a get with pytest.raises(e.coapTimeout): - reply = coap2.GET( + reply, meta = coap2.GET( uri = 'coap://[{0}]:{1}/{2}/'.format(IPADDRESS_INVALID,d.DEFAULT_UDP_PORT,RESOURCE), confirmable = False, options=options