# -*- coding: latin-1 -*- # ----------------------------------------------------------------------------- # Copyright 2012, 2017 Stephen Tiedemann <stephen.tiedemann@gmail.com> # # Licensed under the EUPL, Version 1.1 or - as soon they # will be approved by the European Commission - subsequent # versions of the EUPL (the "Licence"); # You may not use this work except in compliance with the # Licence. # You may obtain a copy of the Licence at: # # https://joinup.ec.europa.eu/software/page/eupl # # Unless required by applicable law or agreed to in # writing, software distributed under the Licence is # distributed on an "AS IS" basis, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either # express or implied. # See the Licence for the specific language governing # permissions and limitations under the Licence. # ----------------------------------------------------------------------------- import logging import re import time import errno import inspect import threading from operator import itemgetter import platform import nfc log = logging.getLogger('main') def get_test_methods(obj): test_methods = list() for name, func in inspect.getmembers(obj, inspect.ismethod): if name.startswith("test_"): line = inspect.getsourcelines(func)[1] text = inspect.getdoc(func) test_methods.append((line, name.lstrip("test_"), text)) return test_methods class TestFail(Exception): def __init__(self, value): self.value = value def __str__(self): return str(self.value) class TestSkip(Exception): def __init__(self, value): self.value = value def __str__(self): return str(self.value) class CommandLineInterface(object): def __init__(self, argument_parser, groups=''): self.groups = groups.split() self.test_completed = False for group in self.groups: eval("self.add_{0}_options".format(group))(argument_parser) argument_parser.add_argument( "-l", "--loop", action="store_true", help="restart after termination") self.options = argument_parser.parse_args() lvl = logging.ERROR if self.options.quiet else logging.INFO if self.options.debug and not self.options.logfile: lvl = logging.DEBUG - (1 if self.options.verbose else 0) fmt = '[%(name)s] %(message)s' if self.options.reltime: fmt = '%(relativeCreated)d ms ' + fmt if self.options.abstime: fmt = '%(asctime)s ' + fmt ch = ColorStreamHandler() ch.setLevel(lvl) ch.setFormatter(logging.Formatter(fmt)) logging.getLogger().addHandler(ch) if self.options.logfile: fmt = '%(asctime)s [%(name)s] %(message)s' fh = logging.FileHandler(self.options.logfile, "w") fh.setFormatter(logging.Formatter(fmt)) fh.setLevel(logging.DEBUG - (1 if self.options.verbose else 0)) logging.getLogger().addHandler(fh) logging.getLogger().setLevel(logging.NOTSET) logging.getLogger('main').setLevel(logging.INFO) for module in self.options.debug: log.info("enable debug output for '{0}'".format(module)) logging.getLogger(module).setLevel(1) log.debug(self.options) if "test" in self.groups: if self.options.test_all: # get_test_method() yields (line, name, docstr), ..., ... test_methods = sorted(get_test_methods(self), key=itemgetter(0)) self.options.test = list(map(itemgetter(1), test_methods)) if len(self.options.test) > 0 and self.options.select: self.options.test = filter( lambda name: re.match(self.options.select, name), self.options.test) def add_dbg_options(self, argument_parser): group = argument_parser.add_argument_group(title="Debug Options") group.add_argument( "-d", metavar="MODULE", dest="debug", action="append", default=list(), help="enable debug log for MODULE (main, nfc.clf, ...)") group.add_argument( "-v", "--verbose", action="store_true", help="show more information") group.add_argument( "-q", "--quiet", action="store_true", help="show less information") group.add_argument( "-f", dest="logfile", metavar="LOGFILE", help="write debug logs to LOGFILE (with date and time)") group.add_argument( "--reltime", action="store_true", help="show relative timestamps in screen log") group.add_argument( "--abstime", action="store_true", help="show absolute timestamps in screen log") def add_llcp_options(self, argument_parser): group = argument_parser.add_argument_group(title="Peer Mode Options") group.add_argument( "--miu", type=int, default=2175, metavar='', help="LLC Link MIU octets (default: %(default)s octets)") group.add_argument( "--lto", type=int, default=500, metavar='', help="LLC Link Timeout in ms (default: %(default)s ms)") group.add_argument( "--lsc", type=int, choices=range(3), default=3, metavar='', help="LLC Link Service Class (default: %(default)s)") group.add_argument( "--rwt", type=int, default=8, metavar='', help="DEP Response Waiting Time index (default: %(default)s)") group.add_argument( "--mode", choices=["t", "target", "i", "initiator"], metavar='', help="connect as [t]arget or [i]nitiator (default: both)") group.add_argument( "--bitrate", type=int, default=424, metavar='', choices=(106, 212, 424), help="""\ DEP Initiator bitrate 106/212/424 (default: %(default)s)""") group.add_argument( "--passive-only", action="store_true", help="only passive mode activation when initiator") group.add_argument( "--listen-time", type=int, default=250, metavar='', help="DEP Target listen time in ms (default: %(default)s ms)") group.add_argument( "--no-aggregation", action="store_true", help="disable outbound packet aggregation") group.add_argument( "--no-encryption", action="store_true", help="disable secure data transport") def add_rdwr_options(self, argument_parser): group = argument_parser.add_argument_group(title="Reader Mode Options") group.add_argument( "--wait", action="store_true", help="wait until tag removed (implicit with '-l')") group.add_argument( "--technology", choices=list("ABFabf"), metavar="{A,B,F}", help="poll for a single technology (default: all)") def add_card_options(self, argument_parser): argument_parser.add_argument_group(title="Card Mode Options") def add_clf_options(self, argument_parser): group = argument_parser.add_argument_group(title="Device Options") group.add_argument( "--device", metavar="PATH", action="append", help=""" use contactless reader at: 'usb[:vid[:pid]]' (with vendor and product id), 'usb[:bus[:dev]]' (with bus and device number), 'tty:port:driver' (with /dev/tty<port> and <driver>), 'com:port:driver' (with COM<port> and <driver>), 'udp[:host[:port]]' (with <host> name/addr and <port> number) """) def add_iop_options(self, argument_parser): group = argument_parser.add_argument_group( title="Interoperability Options") group.add_argument( "--quirks", action="store_true", help="support non-compliant implementations") def add_test_options(self, argument_parser): group = argument_parser.add_argument_group( title="Test options") group.add_argument( "-t", "--test", default=[], action="append", metavar="T", help="add test name <T> to test schedule") group.add_argument( "-T", "--test-all", action="store_true", help="add all available tests to schedule") group.add_argument( "--select", metavar="REGEX", help="from schedule select tests matching REGEX") test_name_and_text, max_name_length = list(), 0 for line, name, text in sorted(get_test_methods(self), key=itemgetter(0)): test_name_and_text.append((name, text.splitlines()[0])) max_name_length = max(max_name_length, len(name)) argument_parser.description += "\nAvailable Tests:\n" for name, text in test_name_and_text: argument_parser.description += ' {0} {1}\n'.format( name.ljust(max_name_length), text) def on_rdwr_startup(self, targets): return targets def on_rdwr_connect(self, tag): log.info(tag) return True def on_llcp_startup(self, llc): if "test" in self.groups and len(self.options.test) == 0: log.error("no test specified") return None return llc def on_llcp_connect(self, llc): if "test" in self.groups: self.test_completed = False threading.Thread(target=self.run_tests, args=(llc,)).start() llc.run(terminate=self.terminate) return False return True def on_card_startup(self, target): log.warning("on_card_startup should be customized") return None def on_card_connect(self, tag): log.info("activated as {0}".format(tag)) if "test" in self.groups: self.test_completed = False self.run_tests(tag) return False return True def on_card_release(self, tag): return True def terminate(self): return self.test_completed def run_tests(self, *args): if len(self.options.test) > 1: log.info("run tests: {0}".format(self.options.test)) for index, test in enumerate(self.options.test): test_name = "test_{0}".format(test) try: test_func = eval("self." + test_name) except AttributeError: log.error("invalid test '{0}'".format(test)) continue test_info = test_func.__doc__.splitlines()[0] try: test_name = "Test {0:02d}".format(test) except ValueError: test_name = test print("{0}: {1}".format(test_name, test_info)) try: test_func(*args) except (TestFail, AssertionError) as error: print("{0}: FAIL ({1})".format(test_name, error)) except TestSkip as error: print("{0}: SKIP ({1})".format(test_name, error)) else: print("{0}: PASS".format(test_name)) if index < len(self.options.test) - 1: time.sleep(1) self.test_completed = True def run_once(self): if self.options.device is None: self.options.device = ['usb'] for path in self.options.device: try: clf = nfc.ContactlessFrontend(path) except IOError as error: if error.errno == errno.ENODEV: log.info("no contactless reader found on " + path) elif error.errno == errno.EACCES: log.info("access denied for device with path " + path) elif error.errno == errno.EBUSY: log.info("the reader on " + path + " is busy") else: log.debug(repr(error) + "when trying " + path) else: log.debug("found a usable reader on " + path) break else: log.error("no contactless reader available") raise SystemExit(1) if "rdwr" in self.groups: rdwr_options = { 'on-startup': self.on_rdwr_startup, 'on-connect': self.on_rdwr_connect, } if self.options.technology: rdwr_options["targets"] = { "A": ["106A"], "B": ["106B"], "F": ["212F"], }[self.options.technology.upper()] else: rdwr_options = None if "llcp" in self.groups: if self.options.mode is None: self.options.role = None elif self.options.mode in ('t', 'target'): self.options.role = 'target' elif self.options.mode in ('i', 'initiator'): self.options.role = 'initiator' llcp_options = { 'on-startup': self.on_llcp_startup, 'on-connect': self.on_llcp_connect, 'role': self.options.role, 'brs': (106, 212, 424).index(self.options.bitrate), 'acm': (not self.options.passive_only), 'rwt': self.options.rwt, 'miu': self.options.miu, 'lto': self.options.lto, 'lsc': self.options.lsc, 'agf': (not self.options.no_aggregation), 'sec': (not self.options.no_encryption), } else: llcp_options = None if "card" in self.groups: card_options = { 'on-startup': self.on_card_startup, 'on-connect': self.on_card_connect, 'on-release': self.on_card_release, 'targets': [], } else: card_options = None try: kwargs = {'llcp': llcp_options, 'rdwr': rdwr_options, 'card': card_options} return clf.connect(**kwargs) finally: clf.close() def run(self): while self.run_once() and self.options.loop: log.info("*** RESTART ***") # ColorStreamHandler for python logging framework. # based on: http://stackoverflow.com/questions/384076/1336640#1336640 # Copyright (c) 2014 Markus Pointner # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. class AnsiColorStreamHandler(logging.StreamHandler): DEFAULT = '\x1b[0m' RED = '\x1b[31m' GREEN = '\x1b[32m' YELLOW = '\x1b[33m' BLUE = '\x1b[34m' CYAN = '\x1b[36m' CRITICAL = RED ERROR = RED WARNING = YELLOW INFO = GREEN DEBUG = CYAN VERBOSE = BLUE @classmethod def _get_color(cls, level): if level >= logging.CRITICAL: return cls.CRITICAL elif level >= logging.ERROR: return cls.ERROR elif level >= logging.WARNING: return cls.WARNING elif level >= logging.INFO: return cls.INFO elif level >= logging.DEBUG: return cls.DEBUG elif level >= logging.DEBUG - 1: return cls.VERBOSE else: return cls.DEFAULT def format(self, record): text = logging.StreamHandler.format(self, record) color = self._get_color(record.levelno) return color + text + self.DEFAULT class WindowsColorStreamHandler(logging.StreamHandler): # wincon.h FOREGROUND_BLACK = 0x0000 FOREGROUND_BLUE = 0x0001 FOREGROUND_GREEN = 0x0002 FOREGROUND_CYAN = 0x0003 FOREGROUND_RED = 0x0004 FOREGROUND_MAGENTA = 0x0005 FOREGROUND_YELLOW = 0x0006 FOREGROUND_GREY = 0x0007 FOREGROUND_INTENSITY = 0x0008 # foreground color is intensified. FOREGROUND_WHITE = FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED BACKGROUND_BLACK = 0x0000 BACKGROUND_BLUE = 0x0010 BACKGROUND_GREEN = 0x0020 BACKGROUND_CYAN = 0x0030 BACKGROUND_RED = 0x0040 BACKGROUND_MAGENTA = 0x0050 BACKGROUND_YELLOW = 0x0060 BACKGROUND_GREY = 0x0070 BACKGROUND_INTENSITY = 0x0080 # background color is intensified. DEFAULT = FOREGROUND_WHITE CRITICAL = BACKGROUND_YELLOW | FOREGROUND_RED | FOREGROUND_INTENSITY \ | BACKGROUND_INTENSITY ERROR = FOREGROUND_RED | FOREGROUND_INTENSITY WARNING = FOREGROUND_YELLOW | FOREGROUND_INTENSITY INFO = FOREGROUND_GREEN DEBUG = FOREGROUND_CYAN VERBOSE = FOREGROUND_BLUE @classmethod def _get_color(cls, level): if level >= logging.CRITICAL: return cls.CRITICAL elif level >= logging.ERROR: return cls.ERROR elif level >= logging.WARNING: return cls.WARNING elif level >= logging.INFO: return cls.INFO elif level >= logging.DEBUG: return cls.DEBUG elif level >= logging.DEBUG - 1: return cls.VERBOSE else: return cls.DEFAULT def _set_color(self, code): import ctypes ctypes.windll.kernel32.SetConsoleTextAttribute(self._outhdl, code) def __init__(self, stream=None): super(WindowsColorStreamHandler, self).__init__(stream) # get file handle for the stream import msvcrt self._outhdl = msvcrt.get_osfhandle(self.stream.fileno()) def emit(self, record): color = self._get_color(record.levelno) self._set_color(color) logging.StreamHandler.emit(self, record) self._set_color(self.FOREGROUND_WHITE) # select ColorStreamHandler based on platform if platform.system() == 'Windows': ColorStreamHandler = WindowsColorStreamHandler else: ColorStreamHandler = AnsiColorStreamHandler