Skip to content

Commit 4ff20dd

Browse files
committed
Rework extensions loading
1 parent 9fa5f34 commit 4ff20dd

2 files changed

Lines changed: 101 additions & 80 deletions

File tree

scapy/config.py

Lines changed: 81 additions & 74 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,85 @@ 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("'%s' does not look like a Scapy plugin !" % extension)
662+
return
663+
try:
664+
scapy_ext_func(ext)
665+
except Exception as ex:
666+
log_loading.warning(
667+
"'%s' failed during initialization with %s" % (
668+
extension,
669+
ex
677670
)
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)
671+
)
672+
return
673+
674+
# Register all the specs provided by this extension
675+
for spec in ext.specs.values():
676+
self._register_spec(spec)
677+
678+
# Add to the extension list
679+
self.exts.append(ext)
680+
681+
# If there are bash autocompletions, add them
682+
if ext.bash_completions:
683+
from scapy.main import _add_bash_autocompletion
684+
685+
for name, script in ext.bash_completions.items():
686+
_add_bash_autocompletion(name, script)
687+
688+
def loadall(self):
689+
"""
690+
Load all extensions registered in conf.
691+
"""
692+
for extension in conf.load_extensions:
693+
self.load(extension)
684694

685695
def __repr__(self):
686696
from scapy.utils import pretty_list
@@ -1033,6 +1043,8 @@ class Conf(ConfClass):
10331043
#: netcache holds time-based caches for net operations
10341044
netcache: NetCache = NetCache()
10351045
geoip_city = None
1046+
#: Scapy extensions that are loaded automatically on load
1047+
load_extensions: List[str] = []
10361048
# can, tls, http and a few others are not loaded by default
10371049
load_layers: List[str] = [
10381050
'bluetooth',
@@ -1170,11 +1182,6 @@ def __getattribute__(self, attr):
11701182

11711183
conf = Conf() # type: Conf
11721184

1173-
# Python 3.8 Only
1174-
if sys.version_info >= (3, 8):
1175-
conf.exts.load()
1176-
1177-
11781185
def crypto_validator(func):
11791186
# type: (DecoratorCallable) -> DecoratorCallable
11801187
"""

scapy/main.py

Lines changed: 20 additions & 6 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
@@ -243,6 +244,12 @@ def _validate_local(k):
243244
# disable INFO: tags related to dependencies missing
244245
# log_loading.setLevel(logging.WARNING)
245246
247+
# extensions to load by default
248+
conf.load_extensions = [
249+
# "scapy-red",
250+
# "scapy-rpc",
251+
]
252+
246253
# force-use libpcap
247254
# conf.use_pcap = True
248255
""".strip()
@@ -261,23 +268,26 @@ def _usage():
261268
sys.exit(0)
262269

263270

264-
def _get_bash_autocompletion_path() -> Optional[pathlib.Path]:
271+
def _add_bash_autocompletion(fname: str, script: pathlib.Path) -> None:
265272
"""
266-
Util function used most notably in setup.py to add a prepare for a bash
267-
autocompletion script.
273+
Util function used most notably in setup.py to add a bash autocompletion script.
268274
"""
269275
try:
276+
# If already defined, exit.
277+
dest = BASH_COMPLETION_FOLDER / fname
278+
if dest.exists():
279+
return
280+
270281
# Check that bash autocompletion folder exists
271282
if not BASH_COMPLETION_FOLDER.exists():
272283
BASH_COMPLETION_FOLDER.mkdir(parents=True, exist_ok=True)
273284
_check_perms(BASH_COMPLETION_FOLDER)
274285

275286
# Copy file
276-
return BASH_COMPLETION_FOLDER
287+
shutil.copy(script, BASH_COMPLETION_FOLDER)
277288
except OSError:
278-
log_loading.warning("Bash autocompletion folder could not be created.",
289+
log_loading.warning("Bash autocompletion script could not be copied.",
279290
exc_info=True)
280-
return None
281291

282292

283293
######################
@@ -851,6 +861,10 @@ def interact(mydict=None,
851861
_locals=SESSION
852862
)
853863

864+
# Load extensions (Python 3.8 Only)
865+
if sys.version_info >= (3, 8):
866+
conf.exts.loadall()
867+
854868
if conf.fancy_banner:
855869
banner_text = get_fancy_banner()
856870
else:

0 commit comments

Comments
 (0)