feat: CLI mode (--create, --reissue, --revoke, --gen-crl)
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import base64
|
||||
import curses
|
||||
import email.encoders
|
||||
@@ -16,6 +17,7 @@ import secrets
|
||||
import shutil
|
||||
import string
|
||||
import subprocess
|
||||
import sys
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from dataclasses import dataclass, field
|
||||
@@ -974,13 +976,136 @@ class CursesApp:
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# === CLI ===
|
||||
# ============================================================
|
||||
|
||||
|
||||
class CliRunner:
|
||||
"""Non-interactive runner for scripting / cron use."""
|
||||
|
||||
def create(self, cn: str, email_addr: str) -> None:
|
||||
self._issue(cn, email_addr, is_renewal=False)
|
||||
|
||||
def reissue(self, cn: str, email_addr: str) -> None:
|
||||
# Prefer the caller-supplied email; fall back to stored metadata.
|
||||
final_email = email_addr or get_email(EASYRSA_PKI_DIR, cn)
|
||||
self._issue(cn, final_email, is_renewal=True)
|
||||
|
||||
def revoke(self, cn: str) -> None:
|
||||
print(f"Revoking {cn}…", file=sys.stderr)
|
||||
try:
|
||||
revoke_issued(EASYRSA_DIR, EASYRSA_PKI_DIR, cn, CA_PASSPHRASE)
|
||||
gen_crl(EASYRSA_DIR, EASYRSA_PKI_DIR, CA_PASSPHRASE)
|
||||
copy_crl(EASYRSA_PKI_DIR, CRL_DEST_PATH)
|
||||
except EasyRSAError as exc:
|
||||
print(f"error: {exc}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
print(f"Revoked {cn}. CRL updated → {CRL_DEST_PATH}")
|
||||
|
||||
def regen_crl(self) -> None:
|
||||
print("Regenerating CRL…", file=sys.stderr)
|
||||
try:
|
||||
gen_crl(EASYRSA_DIR, EASYRSA_PKI_DIR, CA_PASSPHRASE)
|
||||
copy_crl(EASYRSA_PKI_DIR, CRL_DEST_PATH)
|
||||
except EasyRSAError as exc:
|
||||
print(f"error: {exc}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
print(f"CRL updated → {CRL_DEST_PATH}")
|
||||
|
||||
def _issue(self, cn: str, email_addr: str, is_renewal: bool) -> None:
|
||||
password = generate_password()
|
||||
|
||||
if is_renewal:
|
||||
print(f"Revoking existing cert for {cn}…", file=sys.stderr)
|
||||
try:
|
||||
revoke_issued(EASYRSA_DIR, EASYRSA_PKI_DIR, cn, CA_PASSPHRASE)
|
||||
except EasyRSAError as exc:
|
||||
print(f"error during revoke: {exc}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
print(f"Building certificate for {cn}…", file=sys.stderr)
|
||||
try:
|
||||
build_client_full(EASYRSA_DIR, EASYRSA_PKI_DIR, cn, password, CA_PASSPHRASE)
|
||||
except EasyRSAError as exc:
|
||||
if is_renewal:
|
||||
print(
|
||||
f"error during build-client-full: {exc}\n"
|
||||
f"WARNING: {cn} has been revoked but no new cert was built.\n"
|
||||
f"Re-run --reissue for this CN to issue a new certificate.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
else:
|
||||
print(f"error: {exc}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
if email_addr:
|
||||
save_email(EASYRSA_PKI_DIR, cn, email_addr)
|
||||
|
||||
try:
|
||||
ovpn_path = build_ovpn(cn, EASYRSA_PKI_DIR, OVPN_TEMPLATE_PATH,
|
||||
VPN_CONFIGS_DIR, CONFIG_NAME)
|
||||
except Exception as exc:
|
||||
print(f"error building .ovpn: {exc}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
one_time_url = ""
|
||||
try:
|
||||
one_time_url = create_note(password, CRYPTGEON_URL)
|
||||
except CryptgeonError as exc:
|
||||
print(f"warning: Cryptgeon failed: {exc}", file=sys.stderr)
|
||||
|
||||
if email_addr:
|
||||
try:
|
||||
send_email(email_addr, cn, one_time_url or password, ovpn_path,
|
||||
MAIL_FROM, MAIL_SUBJECT, EMAIL_TEMPLATE_PATH, MAIL_BINARY)
|
||||
print(f"Email sent to {email_addr}", file=sys.stderr)
|
||||
except RuntimeError as exc:
|
||||
print(f"warning: email failed: {exc}", file=sys.stderr)
|
||||
|
||||
print(f"config: {ovpn_path}")
|
||||
if one_time_url:
|
||||
print(f"password-url: {one_time_url}")
|
||||
else:
|
||||
print(f"password: {password}")
|
||||
|
||||
|
||||
def _build_parser() -> argparse.ArgumentParser:
|
||||
parser = argparse.ArgumentParser(
|
||||
prog="openvpncertupdate",
|
||||
description="Manage OpenVPN user certificates via EasyRSA.",
|
||||
)
|
||||
group = parser.add_mutually_exclusive_group()
|
||||
group.add_argument("--create", metavar="CN", help="Create a new certificate for CN")
|
||||
group.add_argument("--reissue", metavar="CN", help="Revoke and reissue certificate for CN")
|
||||
group.add_argument("--revoke", metavar="CN", help="Revoke certificate for CN and regenerate CRL")
|
||||
group.add_argument("--gen-crl", action="store_true", help="Regenerate and copy CRL")
|
||||
parser.add_argument("--email", metavar="EMAIL",
|
||||
help="Email address (required for --create; optional for --reissue)")
|
||||
return parser
|
||||
|
||||
|
||||
# ============================================================
|
||||
# === ENTRY POINT ===
|
||||
# ============================================================
|
||||
|
||||
|
||||
def main() -> None:
|
||||
CursesApp().run()
|
||||
parser = _build_parser()
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.create:
|
||||
if not args.email:
|
||||
parser.error("--email is required with --create")
|
||||
CliRunner().create(args.create, args.email)
|
||||
elif args.reissue:
|
||||
CliRunner().reissue(args.reissue, args.email or "")
|
||||
elif args.revoke:
|
||||
CliRunner().revoke(args.revoke)
|
||||
elif args.gen_crl:
|
||||
CliRunner().regen_crl()
|
||||
else:
|
||||
CursesApp().run()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Reference in New Issue
Block a user