Yet Another WebIOPi+
 All Classes Namespaces Files Functions Variables Macros Pages
coap.py
Go to the documentation of this file.
1 # Copyright 2012-2013 Eric Ptak - trouch.com
2 #
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
6 #
7 # http://www.apache.org/licenses/LICENSE-2.0
8 #
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
14 
15 from webiopi.utils.version import PYTHON_MAJOR
16 from webiopi.utils.logger import info, exception
17 
18 import socket
19 import struct
20 import logging
21 import threading
22 
23 M_PLAIN = "text/plain"
24 M_JSON = "application/json"
25 
26 if PYTHON_MAJOR >= 3:
27  from urllib.parse import urlparse
28 else:
29  from urlparse import urlparse
30 
31 try :
32  import _webiopi.GPIO as GPIO
33 except:
34  pass
35 
37  return int(code/100) * 32 + (code%100)
38 
39 
41  FORMATS = {0: "text/plain",
42  40: "application/link-format",
43  41: "application/xml",
44  42: "application/octet-stream",
45  47: "application/exi",
46  50: "application/json"
47  }
48 
49  @staticmethod
50  def getCode(fmt):
51  if fmt == None:
52  return None
53  for code in COAPContentFormat.FORMATS:
54  if COAPContentFormat.FORMATS[code] == fmt:
55  return code
56  return None
57 
58  @staticmethod
59  def toString(code):
60  if code == None:
61  return None
62 
63  if code in COAPContentFormat.FORMATS:
64  return COAPContentFormat.FORMATS[code]
65 
66  raise Exception("Unknown content format %d" % code)
67 
68 
69 class COAPOption():
70  OPTIONS = {1: "If-Match",
71  3: "Uri-Host",
72  4: "ETag",
73  5: "If-None-Match",
74  7: "Uri-Port",
75  8: "Location-Path",
76  11: "Uri-Path",
77  12: "Content-Format",
78  14: "Max-Age",
79  15: "Uri-Query",
80  16: "Accept",
81  20: "Location-Query",
82  35: "Proxy-Uri",
83  39: "Proxy-Scheme"
84  }
85 
86  IF_MATCH = 1
87  URI_HOST = 3
88  ETAG = 4
89  IF_NONE_MATCH = 5
90  URI_PORT = 7
91  LOCATION_PATH = 8
92  URI_PATH = 11
93  CONTENT_FORMAT = 12
94  MAX_AGE = 14
95  URI_QUERY = 15
96  ACCEPT = 16
97  LOCATION_QUERY = 20
98  PROXY_URI = 35
99  PROXY_SCHEME = 39
100 
101 
102 class COAPMessage():
103  TYPES = ["CON", "NON", "ACK", "RST"]
104  CON = 0
105  NON = 1
106  ACK = 2
107  RST = 3
108 
109  def __init__(self, msg_type=0, code=0, uri=None):
110  self.version = 1
111  self.type = msg_type
112  self.code = code
113  self.id = 0
114  self.token = None
115  self.options = []
116  self.host = ""
117  self.port = 5683
118  self.uri_path = ""
119  self.content_format = None
120  self.payload = None
121 
122  if uri != None:
123  p = urlparse(uri)
124  self.host = p.hostname
125  if p.port:
126  self.port = int(p.port)
127  self.uri_path = p.path
128 
129  def __getOptionHeader__(self, byte):
130  delta = (byte & 0xF0) >> 4
131  length = byte & 0x0F
132  return (delta, length)
133 
134  def __str__(self):
135  result = []
136  result.append("Version: %s" % self.version)
137  result.append("Type: %s" % self.TYPES[self.type])
138  result.append("Code: %s" % self.CODES[self.code])
139  result.append("Id: %s" % self.id)
140  result.append("Token: %s" % self.token)
141  #result.append("Options: %s" % len(self.options))
142  #for option in self.options:
143  # result.append("+ %d: %s" % (option["number"], option["value"]))
144  result.append("Uri-Path: %s" % self.uri_path)
145  result.append("Content-Format: %s" % (COAPContentFormat.toString(self.content_format) if self.content_format else M_PLAIN))
146  result.append("Payload: %s" % self.payload)
147  result.append("")
148  return '\n'.join(result)
149 
150  def getOptionHeaderValue(self, value):
151  if value > 268:
152  return 14
153  if value > 12:
154  return 13
155  return value
156 
157  def getOptionHeaderExtension(self, value):
158  buff = bytearray()
159  v = self.getOptionHeaderValue(value)
160 
161  if v == 14:
162  value -= 269
163  buff.append((value & 0xFF00) >> 8)
164  buff.append(value & 0x00FF)
165 
166  elif v == 13:
167  value -= 13
168  buff.append(value)
169 
170  return buff
171 
172  def appendOption(self, buff, lastnumber, option, data):
173  delta = option - lastnumber
174  length = len(data)
175 
176  d = self.getOptionHeaderValue(delta)
177  l = self.getOptionHeaderValue(length)
178 
179  b = 0
180  b |= (d << 4) & 0xF0
181  b |= l & 0x0F
182  buff.append(b)
183 
184  ext = self.getOptionHeaderExtension(delta);
185  for b in ext:
186  buff.append(b)
187 
188  ext = self.getOptionHeaderExtension(length);
189  for b in ext:
190  buff.append(b)
191 
192  for b in data:
193  buff.append(b)
194 
195  return option
196 
197  def getBytes(self):
198  buff = bytearray()
199  byte = (self.version & 0x03) << 6
200  byte |= (self.type & 0x03) << 4
201  if self.token:
202  token_len = min(len(self.token), 8);
203  else:
204  token_len = 0
205  byte |= token_len
206  buff.append(byte)
207  buff.append(self.code)
208  buff.append((self.id & 0xFF00) >> 8)
209  buff.append(self.id & 0x00FF)
210 
211  if self.token:
212  for c in self.token:
213  buff.append(c)
214 
215  lastnumber = 0
216 
217  if len(self.uri_path) > 0:
218  paths = self.uri_path.split("/")
219  for p in paths:
220  if len(p) > 0:
221  if PYTHON_MAJOR >= 3:
222  data = p.encode()
223  else:
224  data = bytearray(p)
225  lastnumber = self.appendOption(buff, lastnumber, COAPOption.URI_PATH, data)
226 
227  if self.content_format != None:
228  data = bytearray()
229  fmt_code = self.content_format
230  if fmt_code > 0xFF:
231  data.append((fmt_code & 0xFF00) >> 8)
232  data.append(fmt_code & 0x00FF)
233  lastnumber = self.appendOption(buff, lastnumber, COAPOption.CONTENT_FORMAT, data)
234 
235  buff.append(0xFF)
236 
237  if self.payload:
238  if PYTHON_MAJOR >= 3:
239  data = self.payload.encode()
240  else:
241  data = bytearray(self.payload)
242  for c in data:
243  buff.append(c)
244 
245  return buff
246 
247  def parseByteArray(self, buff):
248  self.version = (buff[0] & 0xC0) >> 6
249  self.type = (buff[0] & 0x30) >> 4
250  token_length = buff[0] & 0x0F
251  index = 4
252  if token_length > 0:
253  self.token = buff[index:index+token_length]
254 
255  index += token_length
256  self.code = buff[1]
257  self.id = (buff[2] << 8) | buff[3]
258 
259  number = 0
260 
261  # process options
262  while index < len(buff) and buff[index] != 0xFF:
263  (delta, length) = self.__getOptionHeader__(buff[index])
264  offset = 1
265 
266  # delta extended with 1 byte
267  if delta == 13:
268  delta += buff[index+offset]
269  offset += 1
270  # delta extended with 2 buff
271  elif delta == 14:
272  delta += 255 + ((buff[index+offset] << 8) | buff[index+offset+1])
273  offset += 2
274 
275  # length extended with 1 byte
276  if length == 13:
277  length += buff[index+offset]
278  offset += 1
279 
280  # length extended with 2 buff
281  elif length == 14:
282  length += 255 + ((buff[index+offset] << 8) | buff[index+offset+1])
283  offset += 2
284 
285  number += delta
286  valueBytes = buff[index+offset:index+offset+length]
287  # opaque option value
288  if number in [COAPOption.IF_MATCH, COAPOption.ETAG]:
289  value = valueBytes
290  # integer option value
291  elif number in [COAPOption.URI_PORT, COAPOption.CONTENT_FORMAT, COAPOption.MAX_AGE, COAPOption.ACCEPT]:
292  value = 0
293  for b in valueBytes:
294  value <<= 8
295  value |= b
296  # string option value
297  else:
298  if PYTHON_MAJOR >= 3:
299  value = valueBytes.decode()
300  else:
301  value = str(valueBytes)
302  self.options.append({'number': number, 'value': value})
303  index += offset + length
304 
305  index += 1 # skip 0xFF / end-of-options
306 
307  if len(buff) > index:
308  self.payload = buff[index:]
309  else:
310  self.payload = ""
311 
312  for option in self.options:
313  (number, value) = option.values()
314  if number == COAPOption.URI_PATH:
315  self.uri_path += "/%s" % value
316 
317 
319  CODES = {0: None,
320  1: "GET",
321  2: "POST",
322  3: "PUT",
323  4: "DELETE"
324  }
325 
326  GET = 1
327  POST = 2
328  PUT = 3
329  DELETE = 4
330 
331  def __init__(self, msg_type=0, code=0, uri=None):
332  COAPMessage.__init__(self, msg_type, code, uri)
333 
335  def __init__(self, uri):
336  COAPRequest.__init__(self, COAPMessage.CON, COAPRequest.GET, uri)
337 
339  def __init__(self, uri):
340  COAPRequest.__init__(self, COAPMessage.CON, COAPRequest.POST, uri)
341 
343  def __init__(self, uri):
344  COAPRequest.__init__(self, COAPMessage.CON, COAPRequest.PUT, uri)
345 
347  def __init__(self, uri):
348  COAPRequest.__init__(self, COAPMessage.CON, COAPRequest.DELETE, uri)
349 
351  CODES = {0: None,
352  64: "2.00 OK",
353  65: "2.01 Created",
354  66: "2.02 Deleted",
355  67: "2.03 Valid",
356  68: "2.04 Changed",
357  69: "2.05 Content",
358  128: "4.00 Bad Request",
359  129: "4.01 Unauthorized",
360  130: "4.02 Bad Option",
361  131: "4.03 Forbidden",
362  132: "4.04 Not Found",
363  133: "4.05 Method Not Allowed",
364  134: "4.06 Not Acceptable",
365  140: "4.12 Precondition Failed",
366  141: "4.13 Request Entity Too Large",
367  143: "4.15 Unsupported Content-Format",
368  160: "5.00 Internal Server Error",
369  161: "5.01 Not Implemented",
370  162: "5.02 Bad Gateway",
371  163: "5.03 Service Unavailable",
372  164: "5.04 Gateway Timeout",
373  165: "5.05 Proxying Not Supported"
374  }
375 
376  # 2.XX
377  OK = 64
378  CREATED = 65
379  DELETED = 66
380  VALID = 67
381  CHANGED = 68
382  CONTENT = 69
383 
384  # 4.XX
385  BAD_REQUEST = 128
386  UNAUTHORIZED = 129
387  BAD_OPTION = 130
388  FORBIDDEN = 131
389  NOT_FOUND = 132
390  NOT_ALLOWED = 133
391  NOT_ACCEPTABLE = 134
392  PRECONDITION_FAILED = 140
393  ENTITY_TOO_LARGE = 141
394  UNSUPPORTED_CONTENT = 143
395 
396  # 5.XX
397  INTERNAL_ERROR = 160
398  NOT_IMPLEMENTED = 161
399  BAD_GATEWAY = 162
400  SERVICE_UNAVAILABLE = 163
401  GATEWAY_TIMEOUT = 164
402  PROXYING_NOT_SUPPORTED = 165
403 
404  def __init__(self):
405  COAPMessage.__init__(self)
406 
407 class COAPClient():
408  def __init__(self):
409  self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
410  self.socket.settimeout(1.0)
411  self.socket.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 2)
412 
413  def sendRequest(self, message):
414  data = message.getBytes();
415  sent = 0
416  while sent<4:
417  try:
418  self.socket.sendto(data, (message.host, message.port))
419  data = self.socket.recv(1500)
420  response = COAPResponse()
421  response.parseByteArray(bytearray(data))
422  return response
423  except socket.timeout:
424  sent+=1
425  return None
426 
427 class COAPServer(threading.Thread):
428  logger = logging.getLogger("CoAP")
429 
430  def __init__(self, host, port, handler):
431  threading.Thread.__init__(self, name="COAPThread")
432  self.handler = COAPHandler(handler)
433  self.host = host
434  self.port = port
435  self.multicast_ip = '224.0.1.123'
436  if socket.has_ipv6:
437  address_family = socket.AF_INET6
438  else:
439  address_family = socket.AF_INET
440  try:
441  self.socket = socket.socket(address_family, socket.SOCK_DGRAM)
442  except:
443  self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
444  self.socket.bind(('', port))
445  self.socket.settimeout(1)
446  self.running = True
447  self.start()
448 
449  def run(self):
450  info("CoAP Server binded on coap://%s:%s/" % (self.host, self.port))
451  while self.running == True:
452  try:
453  (request, client) = self.socket.recvfrom(1500)
454  requestBytes = bytearray(request)
455  coapRequest = COAPRequest()
456  coapRequest.parseByteArray(requestBytes)
457  coapResponse = COAPResponse()
458  #self.logger.debug("Received Request:\n%s" % coapRequest)
459  self.processMessage(coapRequest, coapResponse)
460  #self.logger.debug("Sending Response:\n%s" % coapResponse)
461  responseBytes = coapResponse.getBytes()
462  self.socket.sendto(responseBytes, client)
463  self.logger.debug('"%s %s CoAP/%.1f" - %s (Client: %s)' % (coapRequest.CODES[coapRequest.code], coapRequest.uri_path, coapRequest.version, coapResponse.CODES[coapResponse.code], client[0]))
464 
465  except socket.timeout as e:
466  continue
467  except Exception as e:
468  if self.running == True:
469  exception(e)
470 
471  info("CoAP Server stopped")
472 
473  def enableMulticast(self):
474  while not self.running:
475  pass
476  mreq = struct.pack("4sl", socket.inet_aton(self.multicast_ip), socket.INADDR_ANY)
477  self.socket.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq)
478  info("CoAP Server binded on coap://%s:%s/ (MULTICAST)" % (self.multicast_ip, self.port))
479 
480  def stop(self):
481  self.running = False
482  self.socket.close()
483 
484  def processMessage(self, request, response):
485  if request.type == COAPMessage.CON:
486  response.type = COAPMessage.ACK
487  else:
488  response.type = COAPMessage.NON
489 
490  if request.token:
491  response.token = request.token
492 
493  response.id = request.id
494  response.uri_path = request.uri_path
495 
496  if request.code == COAPRequest.GET:
497  self.handler.do_GET(request, response)
498  elif request.code == COAPRequest.POST:
499  self.handler.do_POST(request, response)
500  elif request.code / 32 == 0:
501  response.code = COAPResponse.NOT_IMPLEMENTED
502  else:
503  exception(Exception("Received CoAP Response : %s" % response))
504 
505 class COAPHandler():
506  def __init__(self, handler):
507  self.handler = handler
508 
509  def do_GET(self, request, response):
510  try:
511  (code, body, contentType) = self.handler.do_GET(request.uri_path[1:], True)
512  if code == 0:
513  response.code = COAPResponse.NOT_FOUND
514  elif code == 200:
515  response.code = COAPResponse.CONTENT
516  else:
517  response.code = HTTPCode2CoAPCode(code)
518  response.payload = body
519  response.content_format = COAPContentFormat.getCode(contentType)
520  except (GPIO.InvalidDirectionException, GPIO.InvalidChannelException, GPIO.SetupException) as e:
521  response.code = COAPResponse.FORBIDDEN
522  response.payload = "%s" % e
523  except Exception as e:
524  response.code = COAPResponse.INTERNAL_ERROR
525  raise e
526 
527  def do_POST(self, request, response):
528  try:
529  (code, body, contentType) = self.handler.do_POST(request.uri_path[1:], request.payload, True)
530  if code == 0:
531  response.code = COAPResponse.NOT_FOUND
532  elif code == 200:
533  response.code = COAPResponse.CHANGED
534  else:
535  response.code = HTTPCode2CoAPCode(code)
536  response.payload = body
537  response.content_format = COAPContentFormat.getCode(contentType)
538  except (GPIO.InvalidDirectionException, GPIO.InvalidChannelException, GPIO.SetupException) as e:
539  response.code = COAPResponse.FORBIDDEN
540  response.payload = "%s" % e
541  except Exception as e:
542  response.code = COAPResponse.INTERNAL_ERROR
543  raise e
544