170 lines
5.2 KiB
Python
170 lines
5.2 KiB
Python
import itertools
|
|
import logging
|
|
from typing import Iterable, Optional
|
|
|
|
import click
|
|
|
|
from pyhanko import __version__
|
|
from pyhanko.cli._ctx import CLIContext
|
|
from pyhanko.cli.config import CLIRootConfig, parse_cli_config
|
|
from pyhanko.cli.plugin_api import (
|
|
SIGNING_PLUGIN_ENTRY_POINT_GROUP,
|
|
SIGNING_PLUGIN_REGISTRY,
|
|
)
|
|
from pyhanko.cli.runtime import DEFAULT_CONFIG_FILE, logging_setup
|
|
from pyhanko.config.logging import LogConfig, parse_logging_config
|
|
|
|
__all__ = ['cli_root']
|
|
|
|
|
|
@click.group()
|
|
@click.version_option(prog_name='pyHanko', version=__version__)
|
|
@click.option(
|
|
'--config',
|
|
help=(
|
|
'YAML file to load configuration from'
|
|
f'[default: {DEFAULT_CONFIG_FILE}]'
|
|
),
|
|
required=False,
|
|
type=click.File('r'),
|
|
)
|
|
@click.option(
|
|
'--verbose',
|
|
help='Run in verbose mode',
|
|
required=False,
|
|
default=False,
|
|
type=bool,
|
|
is_flag=True,
|
|
)
|
|
@click.option(
|
|
'--no-plugins',
|
|
help='Disable non-builtin plugin loading',
|
|
type=bool,
|
|
is_flag=True,
|
|
)
|
|
@click.pass_context
|
|
def _root(ctx: click.Context, config, verbose, no_plugins):
|
|
config_text = None
|
|
if config is None:
|
|
try:
|
|
with open(DEFAULT_CONFIG_FILE, 'r') as f:
|
|
config_text = f.read()
|
|
config = DEFAULT_CONFIG_FILE
|
|
except FileNotFoundError:
|
|
pass
|
|
except IOError as e:
|
|
raise click.ClickException(
|
|
f"Failed to read {DEFAULT_CONFIG_FILE}: {str(e)}"
|
|
)
|
|
else:
|
|
try:
|
|
config_text = config.read()
|
|
except IOError as e:
|
|
raise click.ClickException(
|
|
f"Failed to read configuration: {str(e)}",
|
|
)
|
|
|
|
ctx.ensure_object(CLIContext)
|
|
ctx_obj: CLIContext = ctx.obj
|
|
cfg: Optional[CLIRootConfig] = None
|
|
if config_text is not None:
|
|
cfg = parse_cli_config(config_text)
|
|
ctx_obj.config = cfg.config
|
|
log_config = cfg.log_config
|
|
else:
|
|
# grab the default
|
|
log_config = parse_logging_config({})
|
|
|
|
from .commands.signing import register
|
|
|
|
plugins_to_register = _load_plugins(cfg, plugins_enabled=not no_plugins)
|
|
register(plugins_to_register)
|
|
|
|
if verbose:
|
|
# override the root logger's logging level, but preserve the output
|
|
root_logger_config = log_config[None]
|
|
log_config[None] = LogConfig(
|
|
level=logging.DEBUG, output=root_logger_config.output
|
|
)
|
|
else:
|
|
# use the root logger's output settings to populate the default
|
|
log_output = log_config[None].output
|
|
# Revinfo fetch logs -> filter by default
|
|
log_config['pyhanko_certvalidator.fetchers'] = LogConfig(
|
|
level=logging.WARNING, output=log_output
|
|
)
|
|
if 'fontTools.subset' not in log_config:
|
|
# the fontTools subsetter has a very noisy INFO log, so
|
|
# set that one to WARNING by default
|
|
log_config['fontTools.subset'] = LogConfig(
|
|
level=logging.WARNING, output=log_output
|
|
)
|
|
|
|
logging_setup(log_config, verbose)
|
|
|
|
if verbose:
|
|
logging.debug("Running with --verbose")
|
|
if config_text is not None:
|
|
logging.debug(f'Finished reading configuration from {config}.')
|
|
else:
|
|
logging.debug('There was no configuration to parse.')
|
|
|
|
|
|
def _load_plugins(root_config: Optional[CLIRootConfig], plugins_enabled: bool):
|
|
import sys
|
|
from importlib import metadata
|
|
|
|
# we always load the default ones
|
|
to_load = [
|
|
'pyhanko.cli.commands.signing.pkcs11_cli:PKCS11Plugin',
|
|
'pyhanko.cli.commands.signing.pkcs11_cli:BEIDPlugin',
|
|
'pyhanko.cli.commands.signing.simple:PKCS12Plugin',
|
|
'pyhanko.cli.commands.signing.simple:PemderPlugin',
|
|
]
|
|
|
|
eps_from_metadata: Iterable[metadata.EntryPoint] = []
|
|
if plugins_enabled:
|
|
if root_config is not None:
|
|
to_load += [str(mod) for mod in root_config.plugin_endpoints]
|
|
|
|
# need to use dict interface for 3.8 interop
|
|
if sys.version_info < (3, 10):
|
|
eps_from_metadata = metadata.entry_points().get(
|
|
SIGNING_PLUGIN_ENTRY_POINT_GROUP, []
|
|
)
|
|
else:
|
|
eps_from_metadata = metadata.entry_points(
|
|
group=SIGNING_PLUGIN_ENTRY_POINT_GROUP
|
|
)
|
|
|
|
# noinspection PyArgumentList
|
|
to_load_as_endpoints: Iterable[metadata.EntryPoint] = [
|
|
metadata.EntryPoint(
|
|
name='',
|
|
value=v,
|
|
group=SIGNING_PLUGIN_ENTRY_POINT_GROUP,
|
|
)
|
|
for v in to_load
|
|
]
|
|
resulting_plugins = list(SIGNING_PLUGIN_REGISTRY)
|
|
seen = set(type(x) for x in SIGNING_PLUGIN_REGISTRY)
|
|
for ep in itertools.chain(to_load_as_endpoints, eps_from_metadata):
|
|
plugin_cls = ep.load()
|
|
if not isinstance(plugin_cls, type):
|
|
click.echo(
|
|
click.style(
|
|
f"Plugins must be defined as references to classes with a "
|
|
f"nullary init function, but '{ep.value}' is "
|
|
f"a {type(plugin_cls)}. Disregarding...",
|
|
bold=True,
|
|
)
|
|
)
|
|
continue
|
|
if plugin_cls not in seen:
|
|
seen.add(plugin_cls)
|
|
resulting_plugins.append(plugin_cls())
|
|
return resulting_plugins
|
|
|
|
|
|
cli_root: click.Group = _root
|