def pretty_print(message): """Given a CoAP message, reshape its payload into something human-readable. The return value is a triple (infos, mime, text) where text represents the payload, mime is a type that could be used to syntax-highlight the text (not necessarily related to the original mime type, eg. a report of some binary data that's shaped like Markdown could use a markdown mime type), and some line of infos that give additional data (like the reason for a hex dump or the original mime type). >>> from aiocoap import Message >>> def build(payload, request_cf, response_cf): ... response = Message(payload=payload, content_format=response_cf) ... request = Message(accept=request_cf) ... response.request = request ... return response >>> pretty_print(Message(payload=b"Hello", content_format=0)) ([], 'text/plain;charset=utf8', 'Hello') >>> print(pretty_print(Message(payload=b'{"hello":"world"}', content_format=50))[-1]) { "hello": "world" } >>> # Erroneous inputs still go to the pretty printer as long as they're >>> #Unicode >>> pretty_print(Message(payload=b'{"hello":"world', content_format=50)) (['Invalid JSON not re-formated'], 'application/json', '{"hello":"world') >>> pretty_print(Message(payload=b'<>,', content_format=40)) (['Invalid application/link-format content was not re-formatted'], 'application/link-format', '<>,') >>> pretty_print(Message(payload=b'a', content_format=60)) # doctest: +ELLIPSIS (['Showing hex dump of application/cbor payload: CBOR value is invalid'], 'text/vnd.aiocoap.hexdump', '00000000 61 ... """ infos = [] info = infos.append cf = message.opt.content_format or message.request.opt.accept if cf is None: content_type = "type unknown" elif cf.is_known(): content_type = cf.media_type if cf.encoding != 'identity': info("Content format is %s in %s encoding; treating as " "application/octet-stream because decompression is not " "supported yet" % (cf.media_type, cf.encoding)) else: content_type = "type %d" % cf category = contenttype.categorize(content_type) show_hex = None if linkformat is not None and category == 'link-format': try: decoded = message.payload.decode('utf8') try: parsed = linkformat.link_header.parse(decoded) except linkformat.link_header.ParseException: info( "Invalid application/link-format content was not re-formatted" ) return (infos, 'application/link-format', decoded) else: info("application/link-format content was re-formatted") prettyprinted = ",\n".join(str(l) for l in parsed.links) return (infos, 'application/link-format', prettyprinted) except ValueError: # Handled later pass elif category == 'cbor': try: parsed = cbor.loads(message.payload) except cbor.CBORDecodeError: show_hex = "CBOR value is invalid" else: info("CBOR message shown in naïve Python decoding") # Formatting it via Python b/c that's reliably available (as # opposed to JSON which might not round-trip well). The repr for # tags might still not be parsable, but I think chances of good # highlighting are best this way # # Not sorting dicts to give a more faithful representation of the # original CBOR message if sys.version_info >= (3, 8): printer = pprint.PrettyPrinter(sort_dicts=False) else: printer = pprint.PrettyPrinter() formatted = printer.pformat(parsed) return (infos, 'text/x-python3', formatted) elif category == 'json': try: decoded = message.payload.decode('utf8') except ValueError: pass else: try: parsed = json.loads(decoded) except ValueError: info("Invalid JSON not re-formated") return (infos, 'application/json', decoded) else: info("JSON re-formated and indented") formatted = json.dumps(parsed, indent=4) return (infos, 'application/json', formatted) # That's about the formats we do for now. if show_hex is None: try: text = message.payload.decode('utf8') except UnicodeDecodeError: show_hex = "Message can not be parsed as UTF-8" else: return (infos, 'text/plain;charset=utf8', text) info("Showing hex dump of %s payload%s" % (content_type if cf is not None else "untyped", ": " + show_hex if show_hex is not None else "")) data = message.payload # Not the most efficient hex dumper, but we won't stream video over # this anyway formatted = [] offset = 0 while data: line, data = data[:16], data[16:] formatted.append("%08x " % offset + " ".join("%02x" % line[i] if i < len(line) else " " for i in range(8)) + " " + " ".join("%02x" % line[i] if i < len(line) else " " for i in range(8, 16)) + " |" + "".join( chr(x) if 32 <= x < 127 else '.' for x in line) + "|\n") offset += len(line) if offset % 16 != 0: formatted.append("%08x\n" % offset) return (infos, MEDIATYPE_HEXDUMP, "".join(formatted))
async def single_request(args, context=None): parser = build_parser() options = parser.parse_args(args) pretty_print_modules = aiocoap.defaults.prettyprint_missing_modules() if pretty_print_modules and \ (options.color is True or options.pretty_print is True): parser.error("Color and pretty printing require the following" " additional module(s) to be installed: %s" % ", ".join(pretty_print_modules)) if options.color is None: options.color = sys.stdout.isatty() and not pretty_print_modules if options.pretty_print is None: options.pretty_print = sys.stdout.isatty() and not pretty_print_modules configure_logging((options.verbose or 0) - (options.quiet or 0)) try: code = getattr(aiocoap.numbers.codes.Code, options.method.upper()) except AttributeError: try: code = aiocoap.numbers.codes.Code(int(options.method)) except ValueError: raise parser.error("Unknown method") if context is None: context = await aiocoap.Context.create_client_context() if options.credentials is not None: apply_credentials(context, options.credentials, parser.error) request = aiocoap.Message( code=code, mtype=aiocoap.NON if options.non else aiocoap.CON) try: request.set_request_uri(options.url) except ValueError as e: raise parser.error(e) if not request.opt.uri_host and not request.unresolved_remote: raise parser.error("Request URLs need to be absolute.") if options.accept: try: request.opt.accept = int(options.accept) except ValueError: try: request.opt.accept = aiocoap.numbers.media_types_rev[ options.accept] except KeyError: raise parser.error("Unknown accept type") if options.observe: request.opt.observe = 0 observation_is_over = asyncio.Future() if options.content_format: try: request.opt.content_format = int(options.content_format) except ValueError: try: request.opt.content_format = aiocoap.numbers.media_types_rev[ options.content_format] except KeyError: raise parser.error("Unknown content format") if options.payload: if options.payload.startswith('@'): filename = options.payload[1:] if filename == "-": f = sys.stdin.buffer else: f = open(filename, 'rb') try: request.payload = f.read() except OSError as e: raise parser.error("File could not be opened: %s" % e) else: if contenttype.categorize( aiocoap.numbers.media_types.get(request.opt.content_format, "")) == 'cbor': try: import cbor2 as cbor except ImportError as e: raise parser.error("CBOR recoding not available (%s)" % e) import json try: decoded = json.loads(options.payload) except json.JSONDecodeError as e: import ast try: decoded = ast.literal_eval(options.payload) except ValueError: raise parser.error( "JSON and Python recoding failed. Make sure quotation marks are escaped from the shell. JSON error: %s" % e) request.payload = cbor.dumps(decoded) else: request.payload = options.payload.encode('utf8') if options.payload_initial_szx is not None: request.opt.block1 = aiocoap.optiontypes.BlockOption.BlockwiseTuple( 0, False, options.payload_initial_szx, ) if options.proxy is None: interface = context else: interface = aiocoap.proxy.client.ProxyForwarder(options.proxy, context) try: requester = interface.request(request) if options.observe: requester.observation.register_errback( observation_is_over.set_result) requester.observation.register_callback( lambda data, options=options: incoming_observation( options, data)) try: response_data = await requester.response except aiocoap.error.ResolutionError as e: print("Name resolution error:", e, file=sys.stderr) sys.exit(1) except aiocoap.error.NetworkError as e: print("Network error:", e, file=sys.stderr) sys.exit(1) # Fallback while not all backends raise NetworkErrors except OSError as e: text = str(e) if not text: text = repr(e) if not text: # eg ConnectionResetError flying out of a misconfigured SSL server text = type(e) print("Error:", text, file=sys.stderr) sys.exit(1) if response_data.code.is_successful(): present(response_data, options) else: print(colored(response_data.code, options, 'red'), file=sys.stderr) present(response_data, options, file=sys.stderr) sys.exit(1) if options.observe: exit_reason = await observation_is_over print("Observation is over: %r" % (exit_reason, ), file=sys.stderr) finally: if not requester.response.done(): requester.response.cancel() if options.observe and not requester.observation.cancelled: requester.observation.cancel()
def pretty_print(message): """Given a CoAP message, reshape its payload into something human-readable. The return value is a triple (infos, mime, text) where text represents the payload, mime is a type that could be used to syntax-highlight the text (not necessarily related to the original mime type, eg. a report of some binary data that's shaped like Markdown could use a markdown mime type), and some line of infos that give additional data (like the reason for a hex dump or the original mime type). """ infos = [] info = lambda m: infos.append(m) cf = message.opt.content_format if cf is None: cf = message.request.opt.accept content_type = media_types.get(cf, "type %s" % cf) category = contenttype.categorize(content_type) show_hex = None if linkformat is not None and category == 'link-format': try: parsed = linkformat.link_header.parse(message.payload.decode('utf8')) except ValueError: pass else: info("application/link-format content was re-formatted") prettyprinted = ",\n".join(str(l) for l in parsed.links) return (infos, 'application/link-format', prettyprinted) elif category == 'cbor': try: parsed = cbor.loads(message.payload) except ValueError: show_hex = "CBOR value is invalid" else: info("CBOR message shown in naïve Python decoding") # Formatting it via Python b/c that's reliably available (as # opposed to JSON which might not round-trip well). The repr for # tags might still not be parsable, but I think chances of good # highlighting are best this way # # Not sorting dicts to give a more faithful representation of the # original CBOR message if sys.version_info >= (3, 8): printer = pprint.PrettyPrinter(sort_dicts=False) else: printer = pprint.PrettyPrinter() formatted = printer.pformat(parsed) return (infos, 'text/x-python3', formatted) elif category == 'json': try: parsed = json.loads(message.payload.decode('utf8')) except ValueError: pass else: info("JSON re-formated and indented") formatted = json.dumps(parsed, indent=4) return (infos, 'application/json', formatted) # That's about the formats we do for now. if show_hex is None: try: text = message.payload.decode('utf8') except UnicodeDecodeError: show_hex = "Message can not be parsed as UTF-8" else: return (infos, 'text/plain;charset=utf8', text) info("Showing hex dump of %s payload%s" % ( content_type if cf is not None else "untyped", ": " + show_hex if show_hex is not None else "")) data = message.payload # Not the most efficient hex dumper, but we won't stream video over # this anyway formatted = [] offset = 0 while data: line, data = data[:16], data[16:] formatted.append("%08x " % offset + \ " ".join("%02x" % line[i] if i < len(line) else " " for i in range(8)) + " " + \ " ".join("%02x" % line[i] if i < len(line) else " " for i in range(8, 16)) + " |" + \ "".join(chr(x) if 32 <= x < 127 else '.' for x in line) + \ "|\n") offset += len(line) if offset % 16 != 0: formatted.append("%08x\n" % offset) return (infos, MEDIATYPE_HEXDUMP, "".join(formatted))