From c43a550ebb71c71edfcb18e652bad8120f6014be Mon Sep 17 00:00:00 2001 From: gpotter2 Date: Mon, 29 Apr 2019 19:22:34 +0200 Subject: [PATCH 1/3] Improve Scapy's shell & config (colored logger+read-only) --- scapy/arch/pcapdnet.py | 7 ++-- scapy/arch/windows/__init__.py | 13 +++++-- scapy/config.py | 68 ++++++++++++++++++++++++---------- scapy/dadict.py | 5 +++ scapy/data.py | 9 ++--- scapy/error.py | 30 +++++++++++++-- scapy/extlib.py | 2 +- scapy/layers/dhcp.py | 6 ++- scapy/main.py | 36 +++++++++++++++--- scapy/themes.py | 4 +- scapy/utils6.py | 2 +- 11 files changed, 133 insertions(+), 49 deletions(-) diff --git a/scapy/arch/pcapdnet.py b/scapy/arch/pcapdnet.py index f9c40ca0cb6..736fd5618e1 100644 --- a/scapy/arch/pcapdnet.py +++ b/scapy/arch/pcapdnet.py @@ -159,11 +159,10 @@ def load_winpcapy(): except OSError: conf.use_winpcapy = False if conf.interactive: - log_loading.warning(conf.color_theme.format( + log_loading.critical( "Npcap/Winpcap is not installed ! See " - "https://scapy.readthedocs.io/en/latest/installation.html#windows", # noqa: E501 - "black+bg_red" - )) + "https://scapy.readthedocs.io/en/latest/installation.html#windows" # noqa: E501 + ) if conf.use_winpcapy: def get_if_list(): diff --git a/scapy/arch/windows/__init__.py b/scapy/arch/windows/__init__.py index 0aa2e08f6e7..b169c96af45 100755 --- a/scapy/arch/windows/__init__.py +++ b/scapy/arch/windows/__init__.py @@ -165,11 +165,18 @@ def _reload(self): env="SystemRoot") if self.wireshark: try: - manu_path = load_manuf(os.path.sep.join(self.wireshark.split(os.path.sep)[:-1]) + os.path.sep + "manuf") # noqa: E501 + new_manuf = load_manuf( + os.path.sep.join( + self.wireshark.split(os.path.sep)[:-1] + ) + os.path.sep + "manuf" + ) except (IOError, OSError): # FileNotFoundError not available on Py2 - using OSError # noqa: E501 log_loading.warning("Wireshark is installed, but cannot read manuf !") # noqa: E501 - manu_path = None - scapy.data.MANUFDB = conf.manufdb = manu_path + new_manuf = None + if new_manuf: + # Inject new ManufDB + conf.manufdb.__dict__.clear() + conf.manufdb.__dict__.update(new_manuf.__dict__) def _exec_cmd(command): diff --git a/scapy/config.py b/scapy/config.py index cd5e20b55cd..f5d25701a52 100755 --- a/scapy/config.py +++ b/scapy/config.py @@ -10,6 +10,7 @@ from __future__ import absolute_import from __future__ import print_function import functools +import logging import os import re import time @@ -18,8 +19,6 @@ from scapy import VERSION, base_classes from scapy.consts import DARWIN, WINDOWS, LINUX, BSD -from scapy.data import ETHER_TYPES, IP_PROTOS, TCP_SERVICES, UDP_SERVICES, \ - MANUFDB from scapy.error import log_scapy, warning, ScapyInvalidPlatformException from scapy.modules import six from scapy.themes import NoTheme, apply_ipython_style @@ -53,7 +52,8 @@ def __str__(self): class Interceptor(object): - def __init__(self, name, default, hook, args=None, kargs=None): + def __init__(self, name=None, default=None, + hook=None, args=None, kargs=None): self.name = name self.intname = "_intercepted_%s" % name self.default = default @@ -76,6 +76,19 @@ def __set__(self, obj, val): self.hook(self.name, val, *self.args, **self.kargs) +def _readonly(name): + default = Conf.__dict__[name].default + Interceptor.set_from_hook(conf, name, default) + raise ValueError("Read-only value !") + + +ReadOnlyAttribute = functools.partial( + Interceptor, + hook=(lambda name, *args, **kwargs: _readonly(name)) +) +ReadOnlyAttribute.__doc__ = "Read-only class attribute" + + class ProgPath(ConfClass): pdfreader = "open" if DARWIN else "xdg-open" psreader = "open" if DARWIN else "xdg-open" @@ -211,6 +224,7 @@ def register(self, cmd): def lsc(): + """Displays Scapy's default commands""" print(repr(conf.commands)) @@ -537,7 +551,7 @@ class Conf(ConfClass): debug_tls:When 1, print some TLS session secrets when they are computed. recv_poll_rate: how often to check for new packets. Defaults to 0.05s. """ - version = VERSION + version = ReadOnlyAttribute("version", VERSION) session = "" interactive = False interactive_shell = "" @@ -548,14 +562,14 @@ class Conf(ConfClass): commands = CommandsList() dot15d4_protocol = None # Used in dot15d4.py logLevel = LogLevel() - checkIPID = 0 - checkIPsrc = 1 - checkIPaddr = 1 + checkIPID = False + checkIPsrc = True + checkIPaddr = True checkIPinIP = True - check_TCPerror_seqack = 0 + check_TCPerror_seqack = False verb = 2 prompt = Interceptor("prompt", ">>> ", _prompt_changer) - promisc = 1 + promisc = True sniff_promisc = 1 raw_layer = None raw_summary = False @@ -574,21 +588,21 @@ class Conf(ConfClass): ".scapy_history")) padding = 1 except_filter = "" - debug_match = 0 - debug_tls = 0 + debug_match = False + debug_tls = False wepkey = "" cache_iflist = {} route = None # Filed by route.py route6 = None # Filed by route6.py - auto_fragment = 1 - debug_dissector = 0 + auto_fragment = True + debug_dissector = False color_theme = Interceptor("color_theme", NoTheme(), _prompt_changer) warning_threshold = 5 prog = ProgPath() resolve = Resolve() noenum = Resolve() emph = Emphasize() - use_pypy = isPyPy() + use_pypy = ReadOnlyAttribute("use_pypy", isPyPy()) use_pcap = Interceptor( "use_pcap", os.getenv("SCAPY_USE_PCAPDNET", "").lower().startswith("y"), @@ -600,12 +614,7 @@ class Conf(ConfClass): use_winpcapy = Interceptor("use_winpcapy", False, _socket_changer) use_npcap = False ipv6_enabled = socket.has_ipv6 - ethertypes = ETHER_TYPES - protocols = IP_PROTOS - services_tcp = TCP_SERVICES - services_udp = UDP_SERVICES extensions_paths = "." - manufdb = MANUFDB stats_classic_protocols = [] stats_dot11_protocols = [] temp_files = [] @@ -626,6 +635,25 @@ class Conf(ConfClass): auto_crop_tables = True recv_poll_rate = 0.05 + def __getattr__(self, attr): + # Those are loded on runtime to avoid import loops + if attr == "manufdb": + from scapy.data import MANUFDB + return MANUFDB + if attr == "ethertypes": + from scapy.data import ETHER_TYPES + return ETHER_TYPES + if attr == "protocols": + from scapy.data import IP_PROTOS + return IP_PROTOS + if attr == "services_udp": + from scapy.data import UDP_SERVICES + return UDP_SERVICES + if attr == "services_tcp": + from scapy.data import TCP_SERVICES + return TCP_SERVICES + return object.__getattr__(self, attr) + if not Conf.ipv6_enabled: log_scapy.warning("IPv6 support disabled in Python. Cannot load Scapy IPv6 layers.") # noqa: E501 @@ -634,7 +662,7 @@ class Conf(ConfClass): Conf.load_layers.remove(m) conf = Conf() -conf.logLevel = 30 # 30=Warning +conf.logLevel = logging.WARNING def crypto_validator(func): diff --git a/scapy/dadict.py b/scapy/dadict.py index b4e062630aa..d1b2bc259fc 100644 --- a/scapy/dadict.py +++ b/scapy/dadict.py @@ -115,3 +115,8 @@ def iterkeys(self): def __len__(self): return len(self.__dict__) + + def __nonzero__(self): + # Always has at least its name + return len(self.__dict__) > 1 + __bool__ = __nonzero__ diff --git a/scapy/data.py b/scapy/data.py index cd7f4659099..9594d63a47a 100644 --- a/scapy/data.py +++ b/scapy/data.py @@ -289,14 +289,11 @@ def load_manuf(filename): if WINDOWS: - ETHER_TYPES = load_ethertypes("ethertypes") IP_PROTOS = load_protocols(os.environ["SystemRoot"] + "\\system32\\drivers\\etc\\protocol") # noqa: E501 TCP_SERVICES, UDP_SERVICES = load_services(os.environ["SystemRoot"] + "\\system32\\drivers\\etc\\services") # noqa: E501 - # Default value, will be updated by arch.windows - try: - MANUFDB = load_manuf(os.environ["ProgramFiles"] + "\\wireshark\\manuf") - except (IOError, OSError): # FileNotFoundError not available on Py2 - using OSError # noqa: E501 - MANUFDB = None + # Default values, will be updated by arch.windows + ETHER_TYPES = DADict() + MANUFDB = ManufDA() else: IP_PROTOS = load_protocols("/etc/protocols") ETHER_TYPES = load_ethertypes("/etc/ethertypes") diff --git a/scapy/error.py b/scapy/error.py index 59413a58139..aa6a08cc93d 100644 --- a/scapy/error.py +++ b/scapy/error.py @@ -56,12 +56,36 @@ def filter(self, record): return 1 +# Inspired from python-colorbg (MIT) +class ScapyColoredFormatter(logging.Formatter): + """A subclass of logging.Formatter that handles colors.""" + levels_colored = { + 'DEBUG': 'reset', + 'INFO': 'reset', + 'WARNING': 'bold+yellow', + 'ERROR': 'bold+red', + 'CRITICAL': 'bold+white+bg_red' + } + + def format(self, record): + message = super(ScapyColoredFormatter, self).format(record) + from scapy.config import conf + message = conf.color_theme.format( + message, + self.levels_colored[record.levelname] + ) + return message + + log_scapy = logging.getLogger("scapy") log_scapy.addHandler(logging.NullHandler()) -log_runtime = logging.getLogger("scapy.runtime") # logs at runtime +# logs at runtime +log_runtime = logging.getLogger("scapy.runtime") log_runtime.addFilter(ScapyFreqFilter()) -log_interactive = logging.getLogger("scapy.interactive") # logs in interactive functions # noqa: E501 -log_loading = logging.getLogger("scapy.loading") # logs when loading Scapy # noqa: E501 +# logs in interactive functions +log_interactive = logging.getLogger("scapy.interactive") +# logs when loading Scapy +log_loading = logging.getLogger("scapy.loading") def warning(x, *args, **kargs): diff --git a/scapy/extlib.py b/scapy/extlib.py index 64bacd95d65..d48f116de80 100644 --- a/scapy/extlib.py +++ b/scapy/extlib.py @@ -54,7 +54,7 @@ def _test_pyx(): if _test_pyx(): PYX = 1 else: - log_loading.warning("PyX dependencies are not installed ! Please install TexLive or MikTeX.") # noqa: E501 + log_loading.info("PyX dependencies are not installed ! Please install TexLive or MikTeX.") # noqa: E501 PYX = 0 except ImportError: log_loading.info("Can't import PyX. Won't be able to use psdump() or pdfdump().") # noqa: E501 diff --git a/scapy/layers/dhcp.py b/scapy/layers/dhcp.py index d901ffe5e93..3265f6360b8 100644 --- a/scapy/layers/dhcp.py +++ b/scapy/layers/dhcp.py @@ -346,8 +346,10 @@ class DHCP(Packet): @conf.commands.register def dhcp_request(iface=None, **kargs): """Send a DHCP discover request and return the answer""" - if conf.checkIPaddr != 0: - warning("conf.checkIPaddr is not 0, I may not be able to match the answer") # noqa: E501 + if conf.checkIPaddr: + warning( + "conf.checkIPaddr is enabled, may not be able to match the answer" + ) if iface is None: iface = conf.iface fam, hw = get_if_raw_hwaddr(iface) diff --git a/scapy/main.py b/scapy/main.py index db009253917..4434d956028 100644 --- a/scapy/main.py +++ b/scapy/main.py @@ -25,11 +25,10 @@ # Never add any global import, in main.py, that would trigger a warning message # noqa: E501 # before the console handlers gets added in interact() from scapy.error import log_interactive, log_loading, log_scapy, \ - Scapy_Exception + Scapy_Exception, ScapyColoredFormatter import scapy.modules.six as six from scapy.themes import DefaultTheme, BlackAndWhite, apply_ipython_style from scapy.consts import WINDOWS -from scapy.config import conf, ConfClass IGNORED = list(six.moves.builtins.__dict__) @@ -273,6 +272,7 @@ def save_session(fname=None, session=None, pickleProto=-1): - session: scapy session to use. If None, the console one will be used - pickleProto: pickle proto version (default: -1 = latest)""" from scapy import utils + from scapy.config import conf, ConfClass if fname is None: fname = conf.session if not fname: @@ -317,6 +317,7 @@ def load_session(fname=None): params: - fname: file to load the scapy session from""" + from scapy.config import conf if fname is None: fname = conf.session try: @@ -341,6 +342,7 @@ def update_session(fname=None): params: - fname: file to load the scapy session from""" + from scapy.config import conf if fname is None: fname = conf.session try: @@ -353,6 +355,7 @@ def update_session(fname=None): def init_session(session_name, mydict=None): + from scapy.config import conf global SESSION global GLOBKEYS @@ -405,6 +408,7 @@ def init_session(session_name, mydict=None): def scapy_delete_temp_files(): + from scapy.config import conf for f in conf.temp_files: try: os.unlink(f) @@ -438,17 +442,37 @@ def _len(line): return lines -def interact(mydict=None, argv=None, mybanner=None, loglevel=20): +def interact(mydict=None, argv=None, mybanner=None, loglevel=logging.INFO): + """Starts Scapy's console.""" global SESSION global GLOBKEYS - console_handler = logging.StreamHandler() - console_handler.setFormatter(logging.Formatter("%(levelname)s: %(message)s")) # noqa: E501 + try: + # colorama is bundled within IPython. + # logging.StreamHandler will be overwritten when called, + # We can't wait for IPython to call it + import colorama + colorama.init() + # Success + console_handler = logging.StreamHandler() + console_handler.setFormatter( + ScapyColoredFormatter( + "%(levelname)s: %(message)s", + ) + ) + except ImportError: + # Failure: ignore colors in the logger + console_handler = logging.StreamHandler() + console_handler.setFormatter( + logging.Formatter( + "%(levelname)s: %(message)s", + ) + ) log_scapy.addHandler(console_handler) from scapy.config import conf - conf.color_theme = DefaultTheme() conf.interactive = True + conf.color_theme = DefaultTheme() if loglevel is not None: conf.logLevel = loglevel diff --git a/scapy/themes.py b/scapy/themes.py index be64aaa663a..f9cbc300648 100644 --- a/scapy/themes.py +++ b/scapy/themes.py @@ -105,9 +105,7 @@ def __getattr__(self, attr): after = self.style_normal elif not isinstance(self, BlackAndWhite) and attr in Color.colors: before = Color.colors[attr][0] - after = "".join(Color.colors[x][0] for x in [ - "normal", "reset", "bg_reset" - ]) + after = Color.colors["normal"][0] else: before = after = "" diff --git a/scapy/utils6.py b/scapy/utils6.py index cf4ef533d65..c83f1fc3982 100644 --- a/scapy/utils6.py +++ b/scapy/utils6.py @@ -269,7 +269,7 @@ def in6_addrtovendor(addr): unknown. """ mac = in6_addrtomac(addr) - if mac is None or conf.manufdb is None: + if mac is None or not conf.manufdb: return None res = conf.manufdb._get_manuf(mac) From 69eb8d843d5f22ea267e686ce191e92a8ae7757b Mon Sep 17 00:00:00 2001 From: gpotter2 Date: Mon, 29 Apr 2019 20:17:01 +0200 Subject: [PATCH 2/3] Replace logLevel changer by an Interceptor --- scapy/config.py | 20 +++++++------------- scapy/error.py | 1 + scapy/main.py | 4 +++- test/linux.uts | 18 ++++++++++-------- 4 files changed, 21 insertions(+), 22 deletions(-) diff --git a/scapy/config.py b/scapy/config.py index f5d25701a52..40af34830dc 100755 --- a/scapy/config.py +++ b/scapy/config.py @@ -10,7 +10,6 @@ from __future__ import absolute_import from __future__ import print_function import functools -import logging import os import re import time @@ -358,15 +357,6 @@ def __repr__(self): return "\n".join(c.summary() for c in self._caches_list) -class LogLevel(object): - def __get__(self, obj, otype): - return obj._logLevel - - def __set__(self, obj, val): - log_scapy.setLevel(val) - obj._logLevel = val - - def _version_checker(module, minver): """Checks that module has a higher version that minver. @@ -518,6 +508,11 @@ def _socket_changer(attr, val): raise +def _loglevel_changer(attr, val): + """Handle a change of conf.logLevel""" + log_scapy.setLevel(val) + + class Conf(ConfClass): """This object contains the configuration of Scapy. session : filename where the session will be saved @@ -561,7 +556,7 @@ class Conf(ConfClass): layers = LayersList() commands = CommandsList() dot15d4_protocol = None # Used in dot15d4.py - logLevel = LogLevel() + logLevel = Interceptor("logLevel", log_scapy.level, _loglevel_changer) checkIPID = False checkIPsrc = True checkIPaddr = True @@ -636,7 +631,7 @@ class Conf(ConfClass): recv_poll_rate = 0.05 def __getattr__(self, attr): - # Those are loded on runtime to avoid import loops + # Those are loaded on runtime to avoid import loops if attr == "manufdb": from scapy.data import MANUFDB return MANUFDB @@ -662,7 +657,6 @@ def __getattr__(self, attr): Conf.load_layers.remove(m) conf = Conf() -conf.logLevel = logging.WARNING def crypto_validator(func): diff --git a/scapy/error.py b/scapy/error.py index aa6a08cc93d..2aaecc23d95 100644 --- a/scapy/error.py +++ b/scapy/error.py @@ -78,6 +78,7 @@ def format(self, record): log_scapy = logging.getLogger("scapy") +log_scapy.setLevel(logging.WARNING) log_scapy.addHandler(logging.NullHandler()) # logs at runtime log_runtime = logging.getLogger("scapy.runtime") diff --git a/scapy/main.py b/scapy/main.py index 4434d956028..cbb963573c4 100644 --- a/scapy/main.py +++ b/scapy/main.py @@ -531,7 +531,9 @@ def interact(mydict=None, argv=None, mybanner=None, loglevel=logging.INFO): "instead.\nAutoCompletion, History are disabled." ) if WINDOWS: - log_loading.warning("IPython not available. On Windows, colors are disabled") # noqa: E501 + log_loading.warning( + "On Windows, colors are also disabled" + ) conf.color_theme = BlackAndWhite() IPYTHON = False else: diff --git a/test/linux.uts b/test/linux.uts index 9b846067559..59ea8991dd1 100644 --- a/test/linux.uts +++ b/test/linux.uts @@ -86,18 +86,20 @@ x is not None and ICMP in x and x[ICMP].type == 0 #_flush_fd(socket.ins) = Test legacy attach_filter function -~ linux needs_root +~ tcpdump +import mock from scapy.arch.common import get_bpf_pointer -old_pypy = conf.use_pypy -conf.use_pypy = True - tcpdump_lines = ['12\n', '40 0 0 12\n', '21 0 5 34525\n', '48 0 0 20\n', '21 6 0 6\n', '21 0 6 44\n', '48 0 0 54\n', '21 3 4 6\n', '21 0 3 2048\n', '48 0 0 23\n', '21 0 1 6\n', '6 0 0 1600\n', '6 0 0 0\n'] -pointer = get_bpf_pointer(tcpdump_lines) -assert six.PY3 or isinstance(pointer, str) -assert six.PY3 or len(pointer) > 1 -conf.use_pypy = old_pypy +@mock.patch("scapy.arch.common.conf", Bunch(use_pypy=True)) +def _test_get_bpf_pointer(): + pointer = get_bpf_pointer(tcpdump_lines) + assert isinstance(pointer, str) + assert len(pointer) > 1 + +if six.PY2: + _test_get_bpf_pointer() = Interface aliases & sub-interfaces ~ linux needs_root From b2a75dfcc7d44b9b5efbec9ed3d7686d7af5a0e0 Mon Sep 17 00:00:00 2001 From: gpotter2 Date: Fri, 24 May 2019 13:18:06 +0200 Subject: [PATCH 3/3] Add headerless mode --- scapy/main.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/scapy/main.py b/scapy/main.py index cbb963573c4..bd0add74b82 100644 --- a/scapy/main.py +++ b/scapy/main.py @@ -110,7 +110,9 @@ def _validate_local(x): def _usage(): print( "Usage: scapy.py [-s sessionfile] [-c new_startup_file] " - "[-p new_prestart_file] [-C] [-P]\n" + "[-p new_prestart_file] [-C] [-P] [-H]\n" + "Args:\n" + "\t-H: header-less start\n" "\t-C: do not read startup file\n" "\t-P: do not read pre-startup file\n" ) @@ -485,10 +487,13 @@ def interact(mydict=None, argv=None, mybanner=None, loglevel=logging.INFO): argv = sys.argv try: - opts = getopt.getopt(argv[1:], "hs:Cc:Pp:d") + opts = getopt.getopt(argv[1:], "hs:Cc:Pp:d:H") for opt, parm in opts[0]: if opt == "-h": _usage() + elif opt == "-H": + conf.fancy_prompt = False + conf.verb = 30 elif opt == "-s": session_name = parm elif opt == "-c":