fix TUI crash on serial consoles; make index.txt the sole email store
curses.use_default_colors() raises on terminals (e.g. serial consoles using TERM=linux/vt100) whose terminfo lacks default-color support; init_colors() now falls back to an explicit black background and caps the "disabled" color to 8-color terminals. --reissue without --email silently sent no mail when the CN wasn't in openvpncertupdate-metadata.json, even though its email was already embedded in the cert's index.txt subject (via EASYRSA_REQ_EMAIL) and the TUI already fell back to it. Since build_client_full() always writes that subject field whenever an email is known, metadata.json was a redundant, driftable copy — remove it and have get_email() read index.txt directly, picking the last non-revoked entry for a CN since index.txt can contain revoked/duplicate lines for the same CN. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -212,6 +212,26 @@ def load_all_certs(pki_dir: str) -> list[CertInfo]:
|
||||
return results
|
||||
|
||||
|
||||
def get_email(pki_dir: str, cn: str) -> str:
|
||||
"""Return the emailAddress from CN's most recent V-status index.txt entry.
|
||||
|
||||
index.txt is append-only, so a CN can have several V-status lines (e.g.
|
||||
issued directly with easyrsa, bypassing this tool's revoke-before-build
|
||||
renewal flow) as well as older R-status (revoked) ones; scan the whole
|
||||
file in order and keep the last non-revoked match, or '' if none.
|
||||
"""
|
||||
found = ""
|
||||
with open(os.path.join(pki_dir, "index.txt")) as fh:
|
||||
for line in fh:
|
||||
parsed = _parse_index_line(line)
|
||||
if parsed is None:
|
||||
continue
|
||||
line_cn, _expiry, email = parsed
|
||||
if line_cn == cn:
|
||||
found = email
|
||||
return found
|
||||
|
||||
|
||||
# ============================================================
|
||||
# === PASSWORD ===
|
||||
# ============================================================
|
||||
@@ -333,32 +353,6 @@ def resolve_ca_passphrase(
|
||||
return configured
|
||||
|
||||
|
||||
# ============================================================
|
||||
# === METADATA ===
|
||||
# ============================================================
|
||||
|
||||
_METADATA_FILE = "openvpncertupdate-metadata.json"
|
||||
|
||||
|
||||
def load_metadata(pki_dir: str) -> dict[str, str]:
|
||||
p = os.path.join(pki_dir, _METADATA_FILE)
|
||||
if not os.path.exists(p):
|
||||
return {}
|
||||
with open(p) as fh:
|
||||
return json.load(fh)
|
||||
|
||||
|
||||
def save_email(pki_dir: str, cn: str, email_addr: str) -> None:
|
||||
data = load_metadata(pki_dir)
|
||||
data[cn] = email_addr
|
||||
with open(os.path.join(pki_dir, _METADATA_FILE), "w") as fh:
|
||||
json.dump(data, fh, indent=2)
|
||||
|
||||
|
||||
def get_email(pki_dir: str, cn: str) -> str:
|
||||
return load_metadata(pki_dir).get(cn, "")
|
||||
|
||||
|
||||
# ============================================================
|
||||
# === CONFIG ===
|
||||
# ============================================================
|
||||
@@ -539,13 +533,20 @@ COLOR_SUCCESS = 6
|
||||
|
||||
def init_colors() -> None:
|
||||
curses.start_color()
|
||||
curses.use_default_colors()
|
||||
curses.init_pair(COLOR_NORMAL, curses.COLOR_WHITE, -1)
|
||||
try:
|
||||
curses.use_default_colors()
|
||||
bg = -1
|
||||
except curses.error:
|
||||
# Terminal (e.g. a serial console) lacks default-color support
|
||||
# (terminfo has no "op"/"AX" capability); fall back to explicit black.
|
||||
bg = curses.COLOR_BLACK
|
||||
disabled_fg = 8 if curses.COLORS > 8 else curses.COLOR_WHITE
|
||||
curses.init_pair(COLOR_NORMAL, curses.COLOR_WHITE, bg)
|
||||
curses.init_pair(COLOR_SELECTED, curses.COLOR_BLACK, curses.COLOR_CYAN)
|
||||
curses.init_pair(COLOR_DISABLED, 8, -1)
|
||||
curses.init_pair(COLOR_TITLE, curses.COLOR_WHITE, -1)
|
||||
curses.init_pair(COLOR_ERROR, curses.COLOR_RED, -1)
|
||||
curses.init_pair(COLOR_SUCCESS, curses.COLOR_GREEN, -1)
|
||||
curses.init_pair(COLOR_DISABLED, disabled_fg, bg)
|
||||
curses.init_pair(COLOR_TITLE, curses.COLOR_WHITE, bg)
|
||||
curses.init_pair(COLOR_ERROR, curses.COLOR_RED, bg)
|
||||
curses.init_pair(COLOR_SUCCESS, curses.COLOR_GREEN, bg)
|
||||
|
||||
|
||||
def draw_box(win, title: str = "") -> None:
|
||||
@@ -1055,11 +1056,8 @@ class CursesApp:
|
||||
elif result.action == Action.NEW_CERT:
|
||||
self._process_cert(stdscr, cn="", email="", is_renewal=False)
|
||||
elif result.action == Action.RENEW_SELECTED:
|
||||
cert_by_cn = {c.cn: c for c in certs}
|
||||
for cn in result.selected_cns:
|
||||
email_addr = get_email(EASYRSA_PKI_DIR, cn)
|
||||
if not email_addr and cn in cert_by_cn:
|
||||
email_addr = cert_by_cn[cn].email
|
||||
if not self._process_cert(stdscr, cn=cn, email=email_addr,
|
||||
is_renewal=True):
|
||||
break
|
||||
@@ -1101,9 +1099,6 @@ class CursesApp:
|
||||
self._error(stdscr, f"EasyRSA error:\n{exc}")
|
||||
return True
|
||||
|
||||
if form.email:
|
||||
save_email(EASYRSA_PKI_DIR, final_cn, form.email)
|
||||
|
||||
try:
|
||||
ovpn_path = build_ovpn(final_cn, EASYRSA_PKI_DIR,
|
||||
OVPN_TEMPLATE_PATH, VPN_CONFIGS_DIR, CONFIG_NAME)
|
||||
@@ -1259,7 +1254,8 @@ class CliRunner:
|
||||
|
||||
def reissue(self, cn: str, email_addr: str, send_email_flag: bool = True,
|
||||
show_eml: bool = False) -> None:
|
||||
# Prefer the caller-supplied email; fall back to stored metadata.
|
||||
# Prefer the caller-supplied email; fall back to the emailAddress
|
||||
# already on the existing cert's subject in index.txt.
|
||||
final_email = email_addr or get_email(EASYRSA_PKI_DIR, cn)
|
||||
self._issue(cn, final_email, is_renewal=True,
|
||||
send_email_flag=send_email_flag, show_eml=show_eml)
|
||||
@@ -1313,9 +1309,6 @@ class CliRunner:
|
||||
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)
|
||||
@@ -1357,6 +1350,9 @@ class CliRunner:
|
||||
print(f"Email sent to {email_addr}", file=sys.stderr)
|
||||
except RuntimeError as exc:
|
||||
print(f"warning: email failed: {exc}", file=sys.stderr)
|
||||
elif send_email_flag and not email_addr:
|
||||
print(f"warning: email skipped (no email address on file for {cn})",
|
||||
file=sys.stderr)
|
||||
|
||||
print(f"config: {ovpn_path}")
|
||||
if one_time_url:
|
||||
|
||||
Reference in New Issue
Block a user