fix: address 7 consistency findings from code review

- Counter overflow: drop len==2 guard in build_ovpn so suffixes grow
  naturally beyond _99 instead of silently resetting to _00
- TOGGLE_ALL: preserve checked set and cursor across A-key view switches
  by threading saved_checked/saved_cursor through ScreenResult and back
  into show_main_screen on the next call
- CLI Cryptgeon fallback: skip email/eml when Cryptgeon fails instead of
  embedding the raw plaintext password in the email body
- --show-eml silent skip: warn explicitly when --show-eml is requested
  but no email address is available
- URL truncation: show full one-time URL in TUI confirmation dialog
- _fatal(): remove misleading alias; inline self._error() + return at the
  sole call site so termination is explicit
- argparse: replace store_true/store_false with store_const for the
  three-state --send-email / --no-send-email / neither pattern

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Vlad Doloman
2026-06-24 11:40:11 +03:00
parent 26f997e5ce
commit eb47e1262f

View File

@@ -277,7 +277,7 @@ def build_ovpn(
int(d.name[len(prefix):])
for d in base.iterdir()
if d.is_dir() and d.name.startswith(prefix)
and d.name[len(prefix):].isdigit() and len(d.name[len(prefix):]) == 2
and d.name[len(prefix):].isdigit()
] if base.exists() else []
next_n = max(existing) + 1 if existing else 0
out_dir = base / f"{prefix}{next_n:02d}"
@@ -661,6 +661,8 @@ class Action(Enum):
class ScreenResult:
action: Action
selected_cns: list[str] = field(default_factory=list)
saved_checked: list[str] = field(default_factory=list)
saved_cursor: int = 0
_MENU_NEW = "Create New Certificate"
@@ -678,7 +680,9 @@ def _expiry_label(c: CertInfo) -> str:
def show_main_screen(
stdscr, certs: list[CertInfo], show_all: bool = False
stdscr, certs: list[CertInfo], show_all: bool = False,
initial_checked: Optional[set[str]] = None,
initial_cursor: int = 0,
) -> ScreenResult:
"""Display cert selection list. Returns when user activates an action."""
init_colors()
@@ -686,8 +690,8 @@ def show_main_screen(
n_certs = len(certs)
n_items = n_certs + len(_MENU_ITEMS)
checked: set[str] = set()
cursor = 0
checked: set[str] = set(initial_checked) if initial_checked else set()
cursor = clamp(initial_cursor, 0, max(0, n_items - 1))
def is_cert(idx): return idx < n_certs
def menu_item(idx): return _MENU_ITEMS[idx - n_certs] if idx >= n_certs else None
@@ -786,7 +790,9 @@ def show_main_screen(
# _MENU_NEW when checked → grayed, ignore
elif key in (ord("a"), ord("A")):
return ScreenResult(action=Action.TOGGLE_ALL)
return ScreenResult(action=Action.TOGGLE_ALL,
saved_checked=sorted(checked),
saved_cursor=cursor)
elif key in (ord("r"), ord("R")) and is_cert(cursor):
cert = certs[cursor]
@@ -811,6 +817,8 @@ class CursesApp:
init_colors()
show_all = False
saved_checked: list[str] = []
saved_cursor: int = 0
while True:
try:
@@ -820,15 +828,21 @@ class CursesApp:
else load_expiring_certs(EASYRSA_PKI_DIR, DAYS_PAST, DAYS_AHEAD)
)
except FileNotFoundError as exc:
self._fatal(stdscr, f"Cannot read PKI index.txt:\n{exc}")
self._error(stdscr, f"Cannot read PKI index.txt:\n{exc}")
return
result = show_main_screen(stdscr, certs, show_all=show_all)
result = show_main_screen(stdscr, certs, show_all=show_all,
initial_checked=set(saved_checked),
initial_cursor=saved_cursor)
saved_checked = []
saved_cursor = 0
if result.action == Action.EXIT:
return
elif result.action == Action.TOGGLE_ALL:
show_all = not show_all
saved_checked = result.saved_checked
saved_cursor = result.saved_cursor
elif result.action == Action.REGEN_CRL:
self._regen_crl(stdscr)
elif result.action == Action.REVOKE:
@@ -900,7 +914,7 @@ class CursesApp:
stdscr,
f"Send email to {form.email}?\n\n"
f"Attachment : {os.path.basename(ovpn_path)}\n"
f"Password URL: {one_time_url[:50]}",
f"Password URL: {one_time_url}",
):
try:
send_email(form.email, final_cn, one_time_url, ovpn_path,
@@ -980,9 +994,6 @@ class CursesApp:
stdscr.refresh()
stdscr.getch()
def _fatal(self, stdscr, text: str) -> None:
self._error(stdscr, text)
def _show_result(
self, stdscr, cn: str, ovpn_path: str, password: str, one_time_url: str,
) -> None:
@@ -1083,9 +1094,14 @@ class CliRunner:
except CryptgeonError as exc:
print(f"warning: Cryptgeon failed: {exc}", file=sys.stderr)
if show_eml and email_addr:
if show_eml:
if not email_addr:
print("warning: --show-eml skipped (no email address)", file=sys.stderr)
elif not one_time_url:
print("warning: --show-eml skipped (Cryptgeon failed)", file=sys.stderr)
else:
try:
msg = build_mime_message(email_addr, cn, one_time_url or password,
msg = build_mime_message(email_addr, cn, one_time_url,
ovpn_path, MAIL_FROM, MAIL_SUBJECT,
EMAIL_TEMPLATE_PATH)
print(base64.b64encode(msg.as_bytes()).decode())
@@ -1093,8 +1109,12 @@ class CliRunner:
print(f"warning: could not build .eml: {exc}", file=sys.stderr)
if email_addr and send_email_flag:
if not one_time_url:
print("warning: email skipped (Cryptgeon failed; use password from stdout)",
file=sys.stderr)
else:
try:
send_email(email_addr, cn, one_time_url or password, ovpn_path,
send_email(email_addr, cn, one_time_url, 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:
@@ -1119,9 +1139,9 @@ def _build_parser() -> argparse.ArgumentParser:
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)")
parser.add_argument("--send-email", dest="send_email", action="store_true",
parser.add_argument("--send-email", dest="send_email", action="store_const", const=True,
default=None, help="Send email after issuing cert (default unless --show-eml)")
parser.add_argument("--no-send-email", dest="send_email", action="store_false",
parser.add_argument("--no-send-email", dest="send_email", action="store_const", const=False,
help="Skip email delivery; print URL to stdout instead")
parser.add_argument("--show-eml", action="store_true",
help="Print the generated email as base64-encoded .eml to stdout "