Coverage for aiocoap/transports/rfc8323common.py: 82%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

90 statements  

1# This file is part of the Python aiocoap library project. 

2# 

3# Copyright (c) 2012-2014 Maciej Wasilak <http://sixpinetrees.blogspot.com/>, 

4# 2013-2014 Christian Amsüss <c.amsuess@energyharvesting.at> 

5# 

6# aiocoap is free software, this file is published under the MIT license as 

7# described in the accompanying LICENSE file. 

8 

9"""Common code for the tcp and the ws modules, both of which are based on 

10RFC8323 mechanisms, but differ in their underlying protocol implementations 

11(asyncio stream vs. websockets module) far enough that they only share small 

12portions of their code""" 

13 

14from typing import Optional 

15from aiocoap import Message 

16from aiocoap import optiontypes, util 

17from aiocoap.numbers.codes import CSM, PING, PONG, RELEASE, ABORT 

18from aiocoap import error 

19 

20class RFC8323Remote: 

21 """Mixin for Remotes for all the common RFC8323 processing 

22 

23 Implementations still need the per-transport parts, especially a 

24 _send_message and an _abort_with implementation. 

25 """ 

26 

27 # CSM received from the peer. The receive hook should abort suitably when 

28 # receiving a non-CSM message and this is not set yet. 

29 _remote_settings: Optional[Message] 

30 

31 # Parameter usually set statically per implementation 

32 _my_max_message_size = 1024 * 1024 

33 

34 def __init__(self): 

35 self._remote_settings = None 

36 

37 is_multicast = False 

38 is_multicast_locally = False 

39 

40 # implementing interfaces.EndpointAddress 

41 

42 def __repr__(self): 

43 return "<%s at %#x, hostinfo %s, local %s>" % (type(self).__name__, id(self), self.hostinfo, self.hostinfo_local) 

44 

45 @property 

46 def hostinfo(self): 

47 # keeping _remote_hostinfo and _local_hostinfo around structurally rather than in 

48 # hostinfo / hostinfo_local form looks odd now, but on the long run the 

49 # remote should be able to tell the message what its default Uri-Host 

50 # value is 

51 return util.hostportjoin(*self._remote_hostinfo) 

52 

53 @property 

54 def hostinfo_local(self): 

55 return util.hostportjoin(*self._local_hostinfo) 

56 

57 @property 

58 def uri_base(self): 

59 if self._is_server: 

60 raise error.AnonymousHost("Client side of %s can not be expressed as a URI" % self._ctx._scheme) 

61 else: 

62 return self._ctx._scheme + '://' + self.hostinfo 

63 

64 @property 

65 def uri_base_local(self): 

66 if self._is_server: 

67 return self._ctx._scheme + '://' + self.hostinfo_local 

68 else: 

69 raise error.AnonymousHost("Client side of %s can not be expressed as a URI" % self._ctx._scheme) 

70 

71 @property 

72 def maximum_block_size_exp(self): 

73 if self._remote_settings is None: 

74 # This is assuming that we can do BERT, so a first Block1 would be 

75 # exponent 7 but still only 1k -- because by the time we send this, 

76 # we typically haven't seen a CSM yet, so we'd be stuck with 6 

77 # because 7959 says we can't increase the exponent... 

78 # 

79 # FIXME: test whether we're properly using lower block sizes if 

80 # server says that szx=7 is not OK. 

81 return 7 

82 

83 max_message_size = (self._remote_settings or {}).get('max-message-size', 1152) 

84 has_blockwise = (self._remote_settings or {}).get('block-wise-transfer', False) 

85 if max_message_size > 1152 and has_blockwise: 

86 return 7 

87 return 6 # FIXME: deal with smaller max-message-size 

88 

89 @property 

90 def maximum_payload_size(self): 

91 max_message_size = (self._remote_settings or {}).get('max-message-size', 1152) 

92 has_blockwise = (self._remote_settings or {}).get('block-wise-transfer', False) 

93 if max_message_size > 1152 and has_blockwise: 

94 return ((max_message_size - 128) // 1024) * 1024 

95 return 1024 # FIXME: deal with smaller max-message-size 

96 

97 # Utility methods for implementing an RFC8323 transport 

98 

99 def _send_initial_csm(self): 

100 my_csm = Message(code=CSM) 

101 # this is a tad awkward in construction because the options objects 

102 # were designed under the assumption that the option space is constant 

103 # for all message codes. 

104 block_length = optiontypes.UintOption(2, self._my_max_message_size) 

105 my_csm.opt.add_option(block_length) 

106 supports_block = optiontypes.UintOption(4, 0) 

107 my_csm.opt.add_option(supports_block) 

108 self._send_message(my_csm) 

109 

110 def _process_signaling(self, msg): 

111 if msg.code == CSM: 

112 if self._remote_settings is None: 

113 self._remote_settings = {} 

114 for opt in msg.opt.option_list(): 

115 # FIXME: this relies on the relevant option numbers to be 

116 # opaque; message parsing should already use the appropriate 

117 # option types, or re-think the way options are parsed 

118 if opt.number == 2: 

119 self._remote_settings['max-message-size'] = int.from_bytes(opt.value, 'big') 

120 elif opt.number == 4: 

121 self._remote_settings['block-wise-transfer'] = True 

122 elif opt.number.is_critical(): 

123 self.abort("Option not supported", bad_csm_option=opt.number) 

124 else: 

125 pass # ignoring elective CSM options 

126 elif msg.code in (PING, PONG, RELEASE, ABORT): 

127 # not expecting data in any of them as long as Custody is not implemented 

128 for opt in msg.opt.option_list(): 

129 if opt.number.is_critical(): 

130 self.abort("Unknown critical option") 

131 else: 

132 pass 

133 

134 if msg.code == PING: 

135 pong = Message(code=PONG, token=msg.token) 

136 self._send_message(pong) 

137 elif msg.code == PONG: 

138 pass 

139 elif msg.code == RELEASE: 

140 raise NotImplementedError 

141 elif msg.code == ABORT: 

142 raise NotImplementedError 

143 else: 

144 self.abort("Unknown signalling code") 

145 

146 def abort(self, errormessage=None, bad_csm_option=None): 

147 self.log.warning("Aborting connection: %s", errormessage) 

148 abort_msg = Message(code=ABORT) 

149 if errormessage is not None: 

150 abort_msg.payload = errormessage.encode('utf8') 

151 if bad_csm_option is not None: 

152 bad_csm_option_option = optiontypes.UintOption(2, bad_csm_option) 

153 abort_msg.opt.add_option(bad_csm_option_option) 

154 self._abort_with(abort_msg)