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