Skip to content

Commit f37c001

Browse files
committed
Rework extensions loading
1 parent 9fa5f34 commit f37c001

3 files changed

Lines changed: 113 additions & 81 deletions

File tree

scapy/config.py

Lines changed: 83 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,11 @@
77
Implementation of the configuration object.
88
"""
99

10-
1110
import atexit
1211
import copy
1312
import functools
1413
import os
14+
import pathlib
1515
import re
1616
import socket
1717
import sys
@@ -538,7 +538,7 @@ def __repr__(self):
538538

539539

540540
class ScapyExt:
541-
__slots__ = ["specs", "name", "version"]
541+
__slots__ = ["specs", "name", "version", "bash_completions"]
542542

543543
class MODE(Enum):
544544
LAYERS = "layers"
@@ -554,6 +554,7 @@ class ScapyExtSpec:
554554

555555
def __init__(self):
556556
self.specs: Dict[str, 'ScapyExt.ScapyExtSpec'] = {}
557+
self.bash_completions = {}
557558

558559
def config(self, name, version):
559560
self.name = name
@@ -576,6 +577,9 @@ def register(self, name, mode, path, default=None):
576577
spec.default = bool(importlib.util.find_spec(spec.fullname))
577578
self.specs[fullname] = spec
578579

580+
def register_bashcompletion(self, script: pathlib.Path):
581+
self.bash_completions[script.name] = script
582+
579583
def __repr__(self):
580584
return "<ScapyExt %s %s (%s specs)>" % (
581585
self.name,
@@ -585,18 +589,20 @@ def __repr__(self):
585589

586590

587591
class ExtsManager(importlib.abc.MetaPathFinder):
588-
__slots__ = ["exts", "_loaded", "all_specs"]
592+
__slots__ = ["exts", "all_specs"]
589593

590-
SCAPY_PLUGIN_CLASSIFIER = 'Framework :: Scapy'
591-
GPLV2_CLASSIFIERS = [
592-
'License :: OSI Approved :: GNU General Public License v2 (GPLv2)',
593-
'License :: OSI Approved :: GNU General Public License v2 or later (GPLv2+)',
594+
GPLV2_LICENCES = [
595+
"GPL-2.0-only",
596+
"GPL-2.0-or-later",
594597
]
595598

596599
def __init__(self):
597600
self.exts: List[ScapyExt] = []
598601
self.all_specs: Dict[str, ScapyExt.ScapyExtSpec] = {}
599602
self._loaded = []
603+
# Add to meta_path as we are an import provider
604+
if self not in sys.meta_path:
605+
sys.meta_path.append(self)
600606

601607
def find_spec(self, fullname, path, target=None):
602608
if fullname in self.all_specs:
@@ -606,81 +612,87 @@ def invalidate_caches(self):
606612
pass
607613

608614
def _register_spec(self, spec):
615+
# Register to known specs
609616
self.all_specs[spec.fullname] = spec
617+
618+
# If default=True, inject it in the currently loaded modules
610619
if spec.default:
611620
loader = importlib.util.LazyLoader(spec.spec.loader)
612621
spec.spec.loader = loader
613622
module = importlib.util.module_from_spec(spec.spec)
614623
sys.modules[spec.fullname] = module
615624
loader.exec_module(module)
616625

617-
def load(self):
626+
def load(self, extension: str):
627+
"""
628+
Load a scapy extension.
629+
630+
:param extension: the name of the extension, as installed.
631+
"""
618632
try:
619633
import importlib.metadata
620634
except ImportError:
635+
raise ImportError("Cannot import importlib.metadata ! Upgrade Python.")
636+
637+
# Get extension distribution
638+
distr = importlib.metadata.distribution(extension)
639+
640+
# Check the classifiers
641+
if distr.metadata.get('License-Expression', None) not in self.GPLV2_LICENCES:
642+
log_loading.warning(
643+
"'%s' has no GPLv2 classifier therefore cannot be loaded." % extension
644+
)
621645
return
622-
for distr in importlib.metadata.distributions():
623-
if any(
624-
v == self.SCAPY_PLUGIN_CLASSIFIER
625-
for k, v in distr.metadata.items() if k == 'Classifier'
626-
):
627-
try:
628-
# Python 3.13 raises an internal warning when calling this
629-
with warnings.catch_warnings():
630-
warnings.filterwarnings("ignore", category=DeprecationWarning)
631-
pkg = next(
632-
k
633-
for k, v in
634-
importlib.metadata.packages_distributions().items()
635-
if distr.name in v
636-
)
637-
except KeyError:
638-
pkg = distr.name
639-
if pkg in self._loaded:
640-
continue
641-
if not any(
642-
v in self.GPLV2_CLASSIFIERS
643-
for k, v in distr.metadata.items() if k == 'Classifier'
644-
):
645-
log_loading.warning(
646-
"'%s' has no GPLv2 classifier therefore cannot be loaded." % pkg # noqa: E501
647-
)
648-
continue
649-
self._loaded.append(pkg)
650-
ext = ScapyExt()
651-
try:
652-
scapy_ext = importlib.import_module(pkg)
653-
except Exception as ex:
654-
log_loading.warning(
655-
"'%s' failed during import with %s" % (
656-
pkg,
657-
ex
658-
)
659-
)
660-
continue
661-
try:
662-
scapy_ext_func = scapy_ext.scapy_ext
663-
except AttributeError:
664-
log_loading.info(
665-
"'%s' included the Scapy Framework specifier "
666-
"but did not include a scapy_ext" % pkg
667-
)
668-
continue
669-
try:
670-
scapy_ext_func(ext)
671-
except Exception as ex:
672-
log_loading.warning(
673-
"'%s' failed during initialization with %s" % (
674-
pkg,
675-
ex
676-
)
646+
647+
# Create the extension
648+
ext = ScapyExt()
649+
650+
# Get the top-level declared "import packages"
651+
# HACK: not available nicely in importlib :/
652+
packages = distr.read_text("top_level.txt").split()
653+
654+
for package in packages:
655+
scapy_ext = importlib.import_module(package)
656+
657+
# We initialize the plugin by calling it's 'scapy_ext' function
658+
try:
659+
scapy_ext_func = scapy_ext.scapy_ext
660+
except AttributeError:
661+
log_loading.warning(
662+
"'%s' does not look like a Scapy plugin !" % extension
663+
)
664+
return
665+
try:
666+
scapy_ext_func(ext)
667+
except Exception as ex:
668+
log_loading.warning(
669+
"'%s' failed during initialization with %s" % (
670+
extension,
671+
ex
677672
)
678-
continue
679-
for spec in ext.specs.values():
680-
self._register_spec(spec)
681-
self.exts.append(ext)
682-
if self not in sys.meta_path:
683-
sys.meta_path.append(self)
673+
)
674+
return
675+
676+
# Register all the specs provided by this extension
677+
for spec in ext.specs.values():
678+
self._register_spec(spec)
679+
680+
# Add to the extension list
681+
self.exts.append(ext)
682+
683+
# If there are bash autocompletions, add them
684+
if ext.bash_completions:
685+
from scapy.main import _add_bash_autocompletion
686+
687+
for name, script in ext.bash_completions.items():
688+
_add_bash_autocompletion(name, script)
689+
690+
def loadall(self) -> None:
691+
"""
692+
Load all extensions registered in conf.
693+
"""
694+
for extension in conf.load_extensions:
695+
self.load(extension)
684696

685697
def __repr__(self):
686698
from scapy.utils import pretty_list
@@ -1033,6 +1045,8 @@ class Conf(ConfClass):
10331045
#: netcache holds time-based caches for net operations
10341046
netcache: NetCache = NetCache()
10351047
geoip_city = None
1048+
#: Scapy extensions that are loaded automatically on load
1049+
load_extensions: List[str] = []
10361050
# can, tls, http and a few others are not loaded by default
10371051
load_layers: List[str] = [
10381052
'bluetooth',
@@ -1170,10 +1184,6 @@ def __getattribute__(self, attr):
11701184

11711185
conf = Conf() # type: Conf
11721186

1173-
# Python 3.8 Only
1174-
if sys.version_info >= (3, 8):
1175-
conf.exts.load()
1176-
11771187

11781188
def crypto_validator(func):
11791189
# type: (DecoratorCallable) -> DecoratorCallable

scapy/main.py

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import os
2020
import pathlib
2121
import pickle
22+
import shutil
2223
import sys
2324
import types
2425
import warnings
@@ -215,7 +216,9 @@ def _validate_local(k):
215216

216217
# https://github.com/scop/bash-completion/blob/main/README.md#faq
217218
if "BASH_COMPLETION_USER_DIR" in os.environ:
218-
BASH_COMPLETION_USER_DIR = pathlib.Path(os.environ["BASH_COMPLETION_USER_DIR"])
219+
BASH_COMPLETION_USER_DIR: Optional[pathlib.Path] = pathlib.Path(
220+
os.environ["BASH_COMPLETION_USER_DIR"]
221+
)
219222
else:
220223
BASH_COMPLETION_USER_DIR = _probe_share_folder("bash-completion")
221224

@@ -243,6 +246,12 @@ def _validate_local(k):
243246
# disable INFO: tags related to dependencies missing
244247
# log_loading.setLevel(logging.WARNING)
245248
249+
# extensions to load by default
250+
conf.load_extensions = [
251+
# "scapy-red",
252+
# "scapy-rpc",
253+
]
254+
246255
# force-use libpcap
247256
# conf.use_pcap = True
248257
""".strip()
@@ -261,23 +270,29 @@ def _usage():
261270
sys.exit(0)
262271

263272

264-
def _get_bash_autocompletion_path() -> Optional[pathlib.Path]:
273+
def _add_bash_autocompletion(fname: str, script: pathlib.Path) -> None:
265274
"""
266-
Util function used most notably in setup.py to add a prepare for a bash
267-
autocompletion script.
275+
Util function used most notably in setup.py to add a bash autocompletion script.
268276
"""
269277
try:
278+
if BASH_COMPLETION_FOLDER is None:
279+
raise OSError()
280+
281+
# If already defined, exit.
282+
dest = BASH_COMPLETION_FOLDER / fname
283+
if dest.exists():
284+
return
285+
270286
# Check that bash autocompletion folder exists
271287
if not BASH_COMPLETION_FOLDER.exists():
272288
BASH_COMPLETION_FOLDER.mkdir(parents=True, exist_ok=True)
273289
_check_perms(BASH_COMPLETION_FOLDER)
274290

275291
# Copy file
276-
return BASH_COMPLETION_FOLDER
292+
shutil.copy(script, BASH_COMPLETION_FOLDER)
277293
except OSError:
278-
log_loading.warning("Bash autocompletion folder could not be created.",
294+
log_loading.warning("Bash autocompletion script could not be copied.",
279295
exc_info=True)
280-
return None
281296

282297

283298
######################
@@ -851,6 +866,10 @@ def interact(mydict=None,
851866
_locals=SESSION
852867
)
853868

869+
# Load extensions (Python 3.8 Only)
870+
if sys.version_info >= (3, 8):
871+
conf.exts.loadall()
872+
854873
if conf.fancy_banner:
855874
banner_text = get_fancy_banner()
856875
else:

scapy/utils.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3891,7 +3891,10 @@ def loop(self, debug: int = 0) -> None:
38913891
print("Output processor failed with error: %s" % ex)
38923892

38933893

3894-
def AutoArgparse(func: DecoratorCallable, _parseonly=False) -> None:
3894+
def AutoArgparse(
3895+
func: DecoratorCallable,
3896+
_parseonly: bool = False,
3897+
) -> Optional[List[Any]]:
38953898
"""
38963899
Generate an Argparse call from a function, then call this function.
38973900

0 commit comments

Comments
 (0)