async def render_post(self, request): query = query_split(request) # this is not deduplicated with update_params in full because that code # path is triggered later when the response was already sent if 'ep' not in query: raise error.BadRequest("ep argument missing") if 'lt' in query: try: _ = int(query['lt']) except ValueError: raise error.BadRequest("lt must be numeric") if 'con' not in query: try: con = request.remote.uri except error.UnparsableMessage: raise error.BadRequest("explicit con required") asyncio.Task( self.process_request( network_remote=request.remote, registration_parameters=query, )) return aiocoap.Message(code=aiocoap.CHANGED)
async def render_post(self, request): query = query_split(request) # this is not deduplicated with update_params in full because that code # path is triggered later when the response was already sent if 'ep' not in query: raise error.BadRequest("ep argument missing") if 'lt' in query: try: # just trying out whether it can be constructed to err out # early and meaningfully int(query['lt']) except ValueError: raise error.BadRequest("lt must be numeric") if 'base' in query: raise error.BadRequest("base is not allowed in simple registrations") try: # just trying out whether it can be constructed to err out in # time (under the current model of "respond early, ask later") request.remote.uri except error.UnparsableMessage: raise error.BadRequest("explicit base required") asyncio.Task(self.process_request( network_remote=request.remote, registration_parameters=query, )) return aiocoap.Message(code=aiocoap.CHANGED)
def update_params(self, network_remote, registration_parameters, is_initial=False): """Set the registration_parameters from the parsed query arguments, update any effects of them, and and trigger any observation observation updates if requried (the typical ones don't because their registration_parameters are {} and all it does is restart the lifetime counter)""" if (is_initial or not self.base_is_explicit) and 'base' not in \ registration_parameters: # check early for validity to avoid side effects of requests # answered with 4.xx try: network_base = network_remote.uri except error.AnonymousHost: raise error.BadRequest("explicit base required") if is_initial: self.registration_parameters = registration_parameters self.lt = 90000 if 'base' not in registration_parameters: self.base = network_base self.base_is_explicit = False # technically might be a re-registration, but we can't catch that at this point actual_change = True else: if 'd' in registration_parameters or 'ep' in registration_parameters: raise error.BadRequest("Parameters 'd' and 'ep' can not be updated") actual_change = any(v != self.registration_parameters[k] for (k, v) in registration_parameters.items()) self.registration_parameters = dict(self.registration_parameters, **registration_parameters) if 'lt' in registration_parameters: try: self.lt = int(registration_parameters['lt']) except ValueError: raise error.BadRequest("lt must be numeric") if 'base' in registration_parameters: self.base = registration_parameters['base'] self.base_is_explicit = True if not self.base_is_explicit and self.base != network_base: self.base = network_base actual_change = True if is_initial: self._set_timeout() else: self.refresh_timeout() if actual_change: self._update_cb()
def update_params(self, network_con, registration_parameters, is_initial=False): """Set the registration_parameters from the parsed query arguments, update any effects of them, and and trigger any observation observation updates if requried (the typical ones don't because their registration_parameters are {} and all it does is restart the lifetime counter)""" if is_initial: self.registration_parameters = registration_parameters self.lt = 86400 self.con_is_explicit = False self.con = network_con # technically might be a re-registration, but we can't catch that at this point actual_change = True else: if 'd' in registration_parameters or 'ep' in registration_parameters: raise error.BadRequest( "Parameters 'd' and 'ep' can not be updated") actual_change = any(v != self.registration_parameters[k] for (k, v) in registration_parameters.items()) self.registration_parameters = dict( self.registration_parameters, **registration_parameters) if 'lt' in registration_parameters: try: self.lt = int(registration_parameters['lt']) except ValueError: raise error.BadRequest("lt must be numeric") if 'con' in registration_parameters: self.con = registration_parameters['con'] self.con_is_explicit = True if not self.con_is_explicit and self.con != network_con: self.con = network_con actual_change = True if is_initial: self._set_timeout() else: self.refresh_timeout() if actual_change: self._update_cb()
def _basic_trust_model(self, payload: StereotypeRequest): if payload.tags.device_class.is_iot(): # Don't expect IoT to be good at responding to or executing tasks task_submission = [0, 1] task_result = [0, 1] else: # All classes of nodes are expected to be good at acknowledging tasks task_submission = [20, 1] # More capable devices are expected to be better a delivering a result if payload.tags.device_class == DeviceClass.RASPBERRY_PI: task_result = [8, 1] elif payload.tags.device_class == DeviceClass.PHONE: task_result = [12, 1] elif payload.tags.device_class == DeviceClass.LAPTOP: task_result = [16, 1] elif payload.tags.device_class == DeviceClass.SERVER: task_result = [20, 1] else: raise error.BadRequest( f"Unknown device class {payload.tags.device_class}") return [task_submission, task_result]
def initialize_endpoint(self, network_remote, registration_parameters): try: ep = registration_parameters['ep'] except KeyError: raise error.BadRequest("ep argument missing") d = registration_parameters.get('d', None) key = (ep, d) try: oldreg = self._endpoint_registrations_by_key[key] except KeyError: path = self._new_pathtail() else: path = oldreg.path[len(self.entity_prefix):] oldreg.delete() # this was the brutal way towards idempotency (delete and re-create). # if any actions based on that are implemented here, they have yet to # decide wheter they'll treat idempotent recreations like deletions or # just ignore them unless something otherwise unchangeable (ep, d) # changes. def delete(): del self._entities_by_pathtail[path] del self._endpoint_registrations_by_key[key] reg = self.Registration(self.entity_prefix + path, network_remote, delete, self._updated_state, registration_parameters) self._endpoint_registrations_by_key[key] = reg self._entities_by_pathtail[path] = reg return reg
async def process_request(self, network_remote, registration_parameters): if 'proxy' not in registration_parameters: try: network_base = network_remote.uri except error.AnonymousHost: raise error.BadRequest("explicit base required") fetch_address = (network_base + '/.well-known/core') get = aiocoap.Message(uri=fetch_address) else: # ignoring that there might be a based present, that will err later get = aiocoap.Message(uri_path=['.well-known', 'core']) get.remote = network_remote get.code = aiocoap.GET get.opt.accept = media_types_rev['application/link-format'] # not trying to catch anything here -- the errors are most likely well renderable into the final response response = await self.context.request(get).response_raising links = link_format_from_message(response) if self.registration_warning: # Conveniently placed so it could be changed to something setting # additional registration_parameters instead logging.warning("Warning from registration: %s", self.registration_warning) registration = self.common_rd.initialize_endpoint( network_remote, registration_parameters) registration.links = links
async def render_post(self, request): self._update_params(request) if request.opt.content_format is not None or request.payload: raise error.BadRequest("Registration update with body not specified") return aiocoap.Message(code=aiocoap.CHANGED)
async def render_get(self, request): """Return stereotype information for the requested Edge server""" if request.opt.content_format != media_types_rev['application/cbor']: raise error.UnsupportedContentFormat() payload = StereotypeRequest.decode(request.payload) if payload.model == TrustModel.No: result = self._no_trust_model(payload) elif payload.model == TrustModel.Basic: result = self._basic_trust_model(payload) elif payload.model == TrustModel.Continuous: result = self._continuous_trust_model(payload) elif payload.model == TrustModel.ChallengeResponse: result = self._challenge_response_trust_model(payload) else: raise error.BadRequest(f"Unknown trust model {payload.model}") result = StereotypeResponse(payload.model, payload.tags, result) result_payload = cbor2.dumps(result.encode()) return aiocoap.Message( payload=result_payload, code=codes.CONTENT, content_format=media_types_rev['application/cbor'])
def link_format_from_message(message): try: if message.opt.content_format == aiocoap.numbers.media_types_rev['application/link-format']: return link_header.parse(message.payload.decode('utf8')) # FIXME this should support json/cbor too else: raise error.UnsupportedMediaType() except (UnicodeDecodeError, link_header.ParseException): raise error.BadRequest()
def pop_single_arg(query, name): """Out of query which is the output of query_split, pick the single value at the key name, raise a suitable BadRequest on error, or return None if nothing is there. The value is removed from the query dictionary.""" if name not in query: return None if len(query[name]) > 1: raise error.BadRequest("Multiple values for %r" % name) return query.pop(name)[0]
def _paginate(candidates, query): try: candidates = list(candidates) if 'page' in query: candidates = candidates[int(query['page']) * int(query['count']):] if 'count' in query: candidates = candidates[:int(query['count'])] except (KeyError, ValueError): raise error.BadRequest("page requires count, and both must be ints") return candidates
def link_format_from_message(message): try: if message.opt.content_format == media_types_rev['application/link-format']: return parse(message.payload.decode('utf8')) elif message.opt.content_format == media_types_rev['application/link-format+json']: return LinkFormat.from_json_string(message.payload.decode('utf8')) elif message.opt.content_format == media_types_rev['application/link-format+cbor']: return LinkFormat.from_cbor_bytes(message.payload) else: raise error.UnsupportedMediaType() except (UnicodeDecodeError, link_header.ParseException): raise error.BadRequest()
async def render_post(self, request): query = query_split(request) if 'base' in query: raise error.BadRequest( "base is not allowed in simple registrations") await self.process_request( network_remote=request.remote, registration_parameters=query, ) return aiocoap.Message(code=aiocoap.CHANGED)
def _paginate(candidates, query): page = pop_single_arg(query, 'page') count = pop_single_arg(query, 'count') try: candidates = list(candidates) if page is not None: candidates = candidates[int(page) * int(count):] if count is not None: candidates = candidates[:int(count)] except (KeyError, ValueError): raise error.BadRequest("page requires count, and both must be ints") return candidates
def link_format_from_message(message): """Convert a response message into a LinkFormat object This expects an explicit media type set on the response (or was explicitly requested) """ certain_format = message.opt.content_format if certain_format is None: certain_format = message.request.opt.accept try: if certain_format == ContentFormat.LINKFORMAT: return parse(message.payload.decode('utf8')) else: raise error.UnsupportedMediaType() except (UnicodeDecodeError, link_header.ParseException): raise error.BadRequest()
def link_format_from_message(message): """Convert a response message into a LinkFormat object This expects an explicit media type set on the response (or was explicitly requested) """ certain_format = message.opt.content_format if certain_format is None: certain_format = message.request.opt.accept try: if certain_format == media_types_rev['application/link-format']: return parse(message.payload.decode('utf8')) elif certain_format == media_types_rev['application/link-format+json']: return LinkFormat.from_json_string(message.payload.decode('utf8')) elif certain_format == media_types_rev['application/link-format+cbor']: return LinkFormat.from_cbor_bytes(message.payload) else: raise error.UnsupportedMediaType() except (UnicodeDecodeError, link_header.ParseException): raise error.BadRequest()
def initialize_endpoint(self, network_remote, registration_parameters): # copying around for later use in static, but not checking again # because reading them from the original will already have screamed by # the time this is used ep_and_d = { k: v for (k, v) in registration_parameters.items() if k in ('ep', 'd') } ep = pop_single_arg(registration_parameters, 'ep') if ep is None: raise error.BadRequest("ep argument missing") d = pop_single_arg(registration_parameters, 'd') key = (ep, d) try: oldreg = self._by_key[key] except KeyError: path = self._new_pathtail() else: path = oldreg.path[len(self.entity_prefix):] oldreg.delete() # this was the brutal way towards idempotency (delete and re-create). # if any actions based on that are implemented here, they have yet to # decide wheter they'll treat idempotent recreations like deletions or # just ignore them unless something otherwise unchangeable (ep, d) # changes. def delete(): del self._by_path[path] del self._by_key[key] reg = self.Registration(ep_and_d, self.entity_prefix + path, network_remote, delete, self._updated_state, registration_parameters) self._by_key[key] = reg self._by_path[path] = reg return reg
async def process_request(self, network_remote, registration_parameters): if 'proxy' not in registration_parameters: try: network_base = network_remote.uri except error.AnonymousHost: raise error.BadRequest("explicit base required") fetch_address = (network_base + '/.well-known/core') get = aiocoap.Message(code=aiocoap.GET, uri=fetch_address) else: # ignoring that there might be a based present, that will err later get = aiocoap.Message(code=aiocoap.GET, uri_path=['.well-known', 'core']) get.remote = network_remote # not trying to catch anything here -- the errors are most likely well renderable into the final response response = await self.context.request(get).response_raising links = link_format_from_message(response) registration = self.common_rd.initialize_endpoint( network_remote, registration_parameters) registration.links = links
def update_params(self, network_remote, registration_parameters, is_initial=False): """Set the registration_parameters from the parsed query arguments, update any effects of them, and and trigger any observation observation updates if requried (the typical ones don't because their registration_parameters are {} and all it does is restart the lifetime counter)""" if any(k in ('ep', 'd') for k in registration_parameters.keys()): # The ep and d of initial registrations are already popped out raise error.BadRequest( "Parameters 'd' and 'ep' can not be updated") # Not in use class "R" or otherwise conflict with common parameters if any(k in ('page', 'count', 'rt', 'href', 'anchor') for k in registration_parameters.keys()): raise error.BadRequest("Unsuitable parameter for registration") if (is_initial or not self.base_is_explicit) and 'base' not in \ registration_parameters: # check early for validity to avoid side effects of requests # answered with 4.xx try: network_base = network_remote.uri except error.AnonymousHost: raise error.BadRequest("explicit base required") if is_initial: # technically might be a re-registration, but we can't catch that at this point actual_change = True else: actual_change = False # Don't act while still checking set_lt = None set_base = None if 'lt' in registration_parameters: try: set_lt = int(pop_single_arg(registration_parameters, 'lt')) except ValueError: raise error.BadRequest("lt must be numeric") if 'base' in registration_parameters: set_base = pop_single_arg(registration_parameters, 'base') if set_lt is not None and self.lt != set_lt: actual_change = True self.lt = set_lt if set_base is not None and (is_initial or self.base != set_base): actual_change = True self.base = set_base self.base_is_explicit = True if not self.base_is_explicit and (is_initial or self.base != network_base): self.base = network_base actual_change = True if any(v != self.registration_parameters.get(k) for (k, v) in registration_parameters.items()): self.registration_parameters.update(registration_parameters) actual_change = True if is_initial: self._set_timeout() else: self.refresh_timeout() if actual_change: self._update_cb()
async def render_to_pipe(self, pipe): request = pipe.request try: unprotected = oscore.verify_start(request) except oscore.NotAProtectedMessage: # ie. if no object_seccurity present await self._inner_site.render_to_pipe(pipe) return if request.code not in (FETCH, POST): raise error.MethodNotAllowed try: sc = self.server_credentials.find_oscore(unprotected) except KeyError: if request.mtype == aiocoap.CON: raise error.Unauthorized("Security context not found") else: return try: unprotected, seqno = sc.unprotect(request) except error.RenderableError as e: # Note that this is flying out of the unprotection (ie. the # security context), which is trusted to not leak unintended # information in unencrypted responses. (By comparison, a # renderable exception flying out of a user # render_to_pipe could only be be rendered to a # protected message, and we'd need to be weary of rendering errors # during to_message as well). # # Note that this clause is not a no-op: it protects the 4.01 Echo # recovery exception (which is also a ReplayError) from being # treated as such. raise e # The other errors could be ported thee but would need some better NoResponse handling. except oscore.ReplayError: if request.mtype == aiocoap.CON: pipe.add_response(aiocoap.Message(code=aiocoap.UNAUTHORIZED, max_age=0, payload=b"Replay detected"), is_last=True) return except oscore.DecodeError: if request.mtype == aiocoap.CON: raise error.BadOption("Failed to decode COSE") else: return except oscore.ProtectionInvalid: if request.mtype == aiocoap.CON: raise error.BadRequest("Decryption failed") else: return unprotected.remote = OSCOREAddress(sc, request.remote) self.log.debug("Request %r was unprotected into %r", request, unprotected) sc = sc.context_for_response() inner_pipe = aiocoap.pipe.IterablePipe(unprotected) pr_that_can_take_errors = aiocoap.pipe.error_to_message( inner_pipe, self.log) # FIXME: do not create a task but run this in here (can this become a # feature of the aiterable PR?) aiocoap.pipe.run_driving_pipe( pr_that_can_take_errors, self._inner_site.render_to_pipe(inner_pipe), name="OSCORE response rendering for %r" % unprotected, ) async for event in inner_pipe: if event.exception is not None: # These are expected to be rare in handlers # # FIXME should we try to render them? (See also # run_driving_pipe). Just raising them # would definitely be bad, as they might be renderable and # then would hit the outer message. self.log.warn( "Turning error raised from renderer into nondescript protected error %r", event.exception) message = aiocoap.Message(code=aiocoap.INTERNAL_SERVER_ERROR) is_last = True else: message = event.message is_last = event.is_last # FIXME: Around several places in the use of pipe (and # now even here), non-final events are hard-coded as observations. # This should shift toward the source telling, or the stream being # annotated as "eventually consistent resource states". if not is_last: message.opt.observe = 0 protected_response, _ = sc.protect(message, seqno) if message.opt.observe is not None: # FIXME: should be done in protect, or by something else that # generally handles obs numbers better (sending the # oscore-reconstructed number is nice because it's consistent # with a proxy that doesn't want to keep a counter when it # knows it's OSCORE already), but starting this per obs with # zero (unless it was done on that token recently) would be # most efficient protected_response.opt.observe = sc.sender_sequence_number & 0xffffffff self.log.debug("Response %r was encrypted into %r", message, protected_response) pipe.add_response(protected_response, is_last=is_last) if event.is_last: break
def initialize_endpoint(self, network_remote, registration_parameters): # copying around for later use in static, but not checking again # because reading them from the original will already have screamed by # the time this is used static_registration_parameters = { k: v for (k, v) in registration_parameters.items() if k in IMMUTABLE_PARAMETERS } ep = pop_single_arg(registration_parameters, 'ep') if ep is None: raise error.BadRequest("ep argument missing") d = pop_single_arg(registration_parameters, 'd') proxy = pop_single_arg(registration_parameters, 'proxy') if proxy is not None and proxy != 'on': raise error.BadRequest("Unsupported proxy value") key = (ep, d) if static_registration_parameters.pop('proxy', None): # FIXME: 'ondemand' is done unconditionally if not self.proxy_domain: raise error.BadRequest("Proxying not enabled") def is_usable(s): # Host names per RFC1123 (which is stricter than what RFC3986 would allow). # # Only supporting lowercase names as to avoid ambiguities due # to hostname capitalizatio normalization (otherwise it'd need # to be first-registered-first-served) return s and all( x in string.ascii_lowercase + string.digits + '-' for x in s) if not is_usable(ep) or (d is not None and not is_usable(d)): raise error.BadRequest( "Proxying only supported for limited ep and d set (lowercase, digits, dash)" ) proxy_host = ep if d is not None: proxy_host += '.' + d proxy_host = proxy_host + '.' + self.proxy_domain else: proxy_host = None # No more errors should fly out from below here, as side effects start now try: oldreg = self._by_key[key] except KeyError: path = self._new_pathtail() else: path = oldreg.path[len(self.entity_prefix):] oldreg.delete() # this was the brutal way towards idempotency (delete and re-create). # if any actions based on that are implemented here, they have yet to # decide wheter they'll treat idempotent recreations like deletions or # just ignore them unless something otherwise unchangeable (ep, d) # changes. def delete(): del self._by_path[path] del self._by_key[key] self.proxy_active.pop(proxy_host) def setproxyremote(remote): self.proxy_active[proxy_host] = remote reg = self.Registration(static_registration_parameters, self.entity_prefix + path, network_remote, delete, self._updated_state, registration_parameters, proxy_host, setproxyremote) self._by_key[key] = reg self._by_path[path] = reg return reg
async def render(self, request): try: recipient_id, id_context = oscore.verify_start(request) except oscore.NotAProtectedMessage: # ie. if no object_seccurity present return await self._inner_site.render(request) try: sc = self.server_credentials.find_oscore(recipient_id, id_context) except KeyError: if request.mtype == aiocoap.CON: raise error.Unauthorized("Security context not found") else: return aiocoap.message.NoResponse try: unprotected, seqno = sc.unprotect(request) except error.RenderableError as e: # Primarily used for the Echo recovery 4.01 reply; the below could # be migrated there, but the behavior (at least as currently # encoded) is not exactly the one a no_response=26 would show, as # we want full responses to CONs but no responses to NONs, wheras # no_response=26 only flushes out an empty ACK and nothing more return e.to_message() except oscore.ReplayError: if request.mtype == aiocoap.CON: return aiocoap.Message(code=aiocoap.UNAUTHORIZED, max_age=0, payload=b"Replay detected") else: return aiocoap.message.NoResponse except oscore.DecodeError: if request.mtype == aiocoap.CON: raise error.BadOption("Failed to decode COSE") else: return aiocoap.message.NoResponse except oscore.ProtectionInvalid as e: if request.mtype == aiocoap.CON: raise error.BadRequest("Decryption failed") else: return aiocoap.message.NoResponse unprotected.remote = OSCOREAddress(sc, request.remote) self.log.debug("Request %r was unprotected into %r", request, unprotected) try: response = await self._inner_site.render(unprotected) except error.RenderableError as err: response = err.to_message() except Exception as err: response = aiocoap.Message(code=aiocoap.INTERNAL_SERVER_ERROR) self.log.error( "An exception occurred while rendering a protected resource: %r", err, exc_info=err) protected_response, _ = sc.protect(response, seqno) self.log.debug("Response %r was encrypted into %r", response, protected_response) return protected_response