Source code for certica.cli

"""
Command-line interface for CA certificate tool
"""

import sys
import click
from pathlib import Path
from .i18n import set_language, t
from .system_check import check_system_requirements
from .ca_manager import CAManager
from .cert_manager import CertManager
from .template_manager import TemplateManager
from .system_cert import SystemCertManager


def _format_path(path: str, base_dir: str = "output") -> str:
    """Format path by removing base_dir prefix for display"""
    try:
        path_str = str(path)
        # Remove base_dir prefix if present
        if path_str.startswith(base_dir + "/") or path_str.startswith(base_dir + "\\"):
            return path_str[len(base_dir) + 1 :]
        # Try with resolved absolute paths
        path_obj = Path(path).resolve()
        base_path = Path(base_dir).resolve()
        try:
            if base_path in path_obj.parents or path_obj == base_path:
                return str(path_obj.relative_to(base_path))
        except ValueError:
            pass
        # If path contains base_dir as substring, try to extract relative part
        path_parts = path_str.split(base_dir)
        if len(path_parts) > 1:
            remaining = path_parts[1].lstrip("/\\")
            return remaining if remaining else path_str
        return path_str
    except Exception:
        # If anything fails, return original path
        return str(path)


@click.group()
@click.option("--base-dir", default="output", help="Base directory for output files")
@click.option("--skip-check", is_flag=True, help="Skip system requirements check")
@click.option("--check-only", is_flag=True, help="Only check system requirements and exit")
@click.pass_context
def cli(ctx, base_dir, skip_check, check_only):
    """CA Certificate Generation Tool - Command Line Interface"""
    # If check-only flag is set, just run check and exit
    if check_only:
        success = check_system_requirements()
        sys.exit(0 if success else 1)

    # Check system requirements unless skipped
    if not skip_check:
        if not check_system_requirements():
            click.echo(f"\n{t('cli.error.system_check_failed')}", err=True)
            click.echo(t("cli.error.system_check_hint"), err=True)
            sys.exit(1)

    ctx.ensure_object(dict)
    ctx.obj["base_dir"] = base_dir
    ctx.obj["ca_manager"] = CAManager(base_dir)
    ctx.obj["cert_manager"] = CertManager(base_dir)
    ctx.obj["template_manager"] = TemplateManager(base_dir)
    ctx.obj["system_cert_manager"] = SystemCertManager()


@cli.command()
@click.option("--name", default="myca", help="CA name")
@click.option("--org", default="Development CA", help="Organization name")
@click.option("--country", default="CN", help="Country code")
@click.option("--state", default="Beijing", help="State/Province")
@click.option("--city", default="Beijing", help="City")
@click.option("--validity", default=3650, type=int, help="Validity in days")
@click.option("--key-size", default=2048, type=int, help="Key size in bits")
@click.option("--template", help="Template file to use for defaults")
@click.pass_context
def create_ca(ctx, name, org, country, state, city, validity, key_size, template):
    """Create a root CA certificate"""
    template_manager = ctx.obj["template_manager"]
    ca_manager = ctx.obj["ca_manager"]

    # Load template if provided
    if template:
        template_data = template_manager.load_template(template)
        org = template_data.get("organization", org)
        country = template_data.get("country", country)
        state = template_data.get("state", state)
        city = template_data.get("city", city)
        validity = template_data.get("default_validity_days", validity)
        key_size = template_data.get("default_key_size", key_size)

    try:
        result = ca_manager.create_root_ca(
            ca_name=name,
            organization=org,
            country=country,
            state=state,
            city=city,
            validity_days=validity,
            key_size=key_size,
        )
        base_dir = ctx.obj["base_dir"]
        click.echo(t("cli.create_ca.success"))
        click.echo(t("cli.create_ca.key", path=_format_path(result["ca_key"], base_dir)))
        click.echo(t("cli.create_ca.cert", path=_format_path(result["ca_cert"], base_dir)))
    except FileExistsError as e:
        click.echo(t("cli.create_ca.error", error=str(e)), err=True)
    except Exception as e:
        click.echo(t("cli.create_ca.error_failed", error=str(e)), err=True)


@cli.command()
@click.option("--ca", required=True, help="CA name to use for signing")
@click.option("--name", required=True, help="Certificate name")
@click.option(
    "--type", type=click.Choice(["server", "client"]), default="server", help="Certificate type"
)
@click.option("--cn", help="Common Name (defaults to certificate name)")
@click.option("--dns", multiple=True, help="DNS names (can be specified multiple times)")
@click.option("--ip", multiple=True, help="IP addresses (can be specified multiple times)")
@click.option("--org", default="Development", help="Organization name")
@click.option("--country", default="CN", help="Country code")
@click.option("--state", default="Beijing", help="State/Province")
@click.option("--city", default="Beijing", help="City")
@click.option("--validity", default=365, type=int, help="Validity in days")
@click.option("--key-size", default=2048, type=int, help="Key size in bits")
@click.option("--template", help="Template file to use for defaults")
@click.pass_context
def sign(ctx, ca, name, type, cn, dns, ip, org, country, state, city, validity, key_size, template):
    """Sign a certificate using the specified CA"""
    template_manager = ctx.obj["template_manager"]
    ca_manager = ctx.obj["ca_manager"]
    cert_manager = ctx.obj["cert_manager"]

    # Get CA
    ca_info = ca_manager.get_ca(ca)
    if not ca_info:
        click.echo(t("cli.sign.error", ca=ca), err=True)
        return

    # Load template if provided
    if template:
        template_data = template_manager.load_template(template)
        org = template_data.get("organization", org)
        country = template_data.get("country", country)
        state = template_data.get("state", state)
        city = template_data.get("city", city)
        validity = template_data.get("default_validity_days", validity)
        key_size = template_data.get("default_key_size", key_size)

    try:
        result = cert_manager.sign_certificate(
            ca_key=ca_info["key"],
            ca_cert=ca_info["cert"],
            ca_name=ca_info["name"],
            cert_name=name,
            cert_type=type,
            common_name=cn or name,
            dns_names=list(dns),
            ip_addresses=list(ip),
            organization=org,
            country=country,
            state=state,
            city=city,
            validity_days=validity,
            key_size=key_size,
        )
        base_dir = ctx.obj["base_dir"]
        click.echo(t("cli.sign.success"))
        click.echo(t("cli.create_ca.key", path=_format_path(result["key"], base_dir)))
        click.echo(t("cli.create_ca.cert", path=_format_path(result["cert"], base_dir)))
    except Exception as e:
        click.echo(t("cli.sign.error_failed", error=str(e)), err=True)


@cli.command()
@click.pass_context
def list_cas(ctx):
    """List all available CA certificates"""
    ca_manager = ctx.obj["ca_manager"]
    cas = ca_manager.list_cas()

    if not cas:
        click.echo(t("cli.list_cas.empty"))
        return

    base_dir = ctx.obj["base_dir"]
    click.echo(f"\n{t('cli.list_cas.title')}")
    for i, ca in enumerate(cas):
        click.echo(f"  {i}. ๐Ÿ”‘ {ca['name']}")
        click.echo(f"     Key: {_format_path(ca['key'], base_dir)}")
        click.echo(f"     Cert: {_format_path(ca['cert'], base_dir)}")


@cli.command()
@click.option("--ca", help="Filter certificates by CA name")
@click.pass_context
def list_certs(ctx, ca):
    """List all signed certificates, optionally filtered by CA"""
    ca_manager = ctx.obj["ca_manager"]
    cert_manager = ctx.obj["cert_manager"]

    if ca:
        # List certificates for specific CA
        ca_info = ca_manager.get_ca(ca)
        if not ca_info:
            click.echo(t("cli.sign.error", ca=ca), err=True)
            return

        certs = ca_manager.get_certs_by_ca(ca)
        if not certs:
            click.echo(t("cli.list_certs.empty_for_ca", ca=ca))
            return

        base_dir = ctx.obj["base_dir"]
        click.echo(f"\n{t('cli.list_certs.title', ca=ca)}")
        for cert in certs:
            click.echo(f"  ๐Ÿ“œ {cert['name']}")
            click.echo(f"     Key: {_format_path(cert['key'], base_dir)}")
            click.echo(f"     Cert: {_format_path(cert['cert'], base_dir)}")
    else:
        # List all certificates
        certs = cert_manager.list_certificates()
        if not certs:
            click.echo(t("cli.list_certs.empty"))
            return

        base_dir = ctx.obj["base_dir"]
        click.echo(f"\n{t('cli.list_certs.title_all')}")
        for cert in certs:
            ca_name = cert.get("ca_name", t("cli.list_certs.ca_unknown"))
            click.echo(f"  ๐Ÿ“œ {cert['name']} (CA: {ca_name})")
            click.echo(f"     Key: {_format_path(cert['key'], base_dir)}")
            click.echo(f"     Cert: {_format_path(cert['cert'], base_dir)}")


@cli.command()
@click.option("--name", required=True, help="Template name")
@click.option("--org", default="Development", help="Organization name")
@click.option("--country", default="CN", help="Country code")
@click.option("--state", default="Beijing", help="State/Province")
@click.option("--city", default="Beijing", help="City")
@click.option("--validity", default=365, type=int, help="Default validity in days")
@click.option("--key-size", default=2048, type=int, help="Default key size in bits")
@click.pass_context
def create_template(ctx, name, org, country, state, city, validity, key_size):
    """Create a template file"""
    template_manager = ctx.obj["template_manager"]
    base_dir = ctx.obj["base_dir"]
    path = template_manager.create_template(name, org, country, state, city, validity, key_size)
    click.echo(t("cli.create_template.success", path=_format_path(path, base_dir)))


@cli.command()
@click.pass_context
def list_templates(ctx):
    """List all available templates"""
    template_manager = ctx.obj["template_manager"]
    templates = template_manager.list_templates()

    if not templates:
        click.echo(t("cli.list_templates.empty"))
        return

    click.echo(f"\n{t('cli.list_templates.title')}")
    for template in templates:
        click.echo(f"  ๐Ÿ“ {template}")


@cli.command()
@click.option("--ca", required=True, help="CA name to install")
@click.option("--password", help="Sudo password (will prompt if not provided)")
@click.pass_context
def install(ctx, ca, password):
    """Install CA certificate to system trust store"""
    ca_manager = ctx.obj["ca_manager"]
    system_cert_manager = ctx.obj["system_cert_manager"]

    ca_info = ca_manager.get_ca(ca)
    if not ca_info:
        click.echo(t("cli.install.error", ca=ca), err=True)
        return

    # Get password if not provided
    if password is None:
        password = click.prompt(t("cli.install.password_prompt"), hide_input=True, default="")
        if not password:
            click.echo(t("cli.install.password_required"), err=True)
            return

    if system_cert_manager.install_ca_cert(ca_info["cert"], ca_info["name"], password):
        click.echo(t("cli.install.success", ca=ca))
    else:
        click.echo(t("cli.install.error_failed"), err=True)


@cli.command()
@click.option("--ca", required=True, help="CA name to remove")
@click.option("--password", help="Sudo password (will prompt if not provided)")
@click.pass_context
def remove(ctx, ca, password):
    """Remove CA certificate from system trust store"""
    system_cert_manager = ctx.obj["system_cert_manager"]

    # Get password if not provided
    if password is None:
        password = click.prompt(t("cli.install.password_prompt"), hide_input=True, default="")
        if not password:
            click.echo(t("cli.install.password_required"), err=True)
            return

    if system_cert_manager.remove_ca_cert(ca, password):
        click.echo(t("cli.remove.success", ca=ca))
    else:
        click.echo(t("cli.remove.error_failed"), err=True)


@cli.command()
@click.option("--cert", required=True, help="Certificate path to show info")
@click.pass_context
def info(ctx, cert):
    """Show certificate information"""
    cert_manager = ctx.obj["cert_manager"]
    info = cert_manager.get_certificate_info(cert)
    click.echo(info["info"])


@cli.command()
@click.option("--lang", "-l", default="en", help="Language code for UI (en, zh, fr, ru, ja, ko)")
@click.pass_context
def ui(ctx, lang):
    """Launch interactive UI mode"""
    from .ui import CAUITool

    # Set language for UI
    if not set_language(lang):
        click.echo(t("lang.unsupported", lang=lang), err=True)
        set_language("en")

    # Check system requirements
    if not check_system_requirements():
        click.echo(f"\n{t('main.error.system_check')}", err=True)
        click.echo(t("main.error.system_check_hint"), err=True)
        sys.exit(1)

    # Launch UI
    base_dir = ctx.obj["base_dir"]
    tool = CAUITool(base_dir=base_dir)
    try:
        tool.run()
    except KeyboardInterrupt:
        click.echo(f"\n\n{t('main.exiting')}")
        sys.exit(0)


[docs] def main(): """Main entry point for CLI""" cli()