mirror of
https://github.com/github/awesome-copilot.git
synced 2026-07-14 01:51:02 +00:00
Fix Namecheap credential handling
Stop persisting Namecheap API keys in ~/.namecheap-api, require the key from environment variables for runtime use, and replace raw exception output in setup/runtime paths with safe guidance. Update the skill documentation to match the new setup flow. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -15,11 +15,11 @@ DO NOT USE FOR: domain registration/purchase, SSL certificate management, hostin
|
||||
|
||||
Before executing any API commands, verify credentials are configured:
|
||||
|
||||
1. **Check for existing config** — look for `~/.namecheap-api`
|
||||
1. **Check for existing config** — look for `~/.namecheap-api` and ensure `NAMECHEAP_API_KEY` is exported in the user's shell
|
||||
2. If not configured, guide the user through setup:
|
||||
a. **Show public IP** — run `python3 namecheap.py public-ip` to display the user's public IP
|
||||
b. **Instruct IP whitelisting** — tell the user to go to https://ap.www.namecheap.com/settings/tools/apiaccess/, enable API (select ON), and whitelist the displayed IP
|
||||
c. **Have the user run setup themselves** — ask the user to run `python3 namecheap.py setup` directly **in their own terminal**. The script prompts for the username and reads the API key with a hidden prompt (`getpass`), writes `~/.namecheap-api` with `chmod 600`, and validates the connection. **Never ask the user to paste their API key into the chat, and never log, echo, or display the API key value.** If you cannot run an interactive terminal for the user, instruct them to run `setup` themselves, or to export `NAMECHEAP_API_USER` and `NAMECHEAP_API_KEY` as environment variables in their own shell — rather than collecting the secret via `ask_user`.
|
||||
c. **Have the user run setup themselves** — ask the user to run `python3 namecheap.py setup` directly **in their own terminal**. The script prompts for the username, reads the API key with a hidden prompt (`getpass`), saves only the username to `~/.namecheap-api` with `chmod 600`, and validates the connection without writing the API key to disk. **Never ask the user to paste their API key into the chat, and never log, echo, or display the API key value.** After setup, the user must export `NAMECHEAP_API_KEY` in their own shell before running API commands. If you cannot run an interactive terminal for the user, instruct them to run `setup` themselves, or to export `NAMECHEAP_API_USER` and `NAMECHEAP_API_KEY` in their own shell — rather than collecting the secret via `ask_user`.
|
||||
d. **Confirm** — once the user reports setup succeeded, proceed with DNS operations.
|
||||
|
||||
### DNS Operations
|
||||
@@ -102,7 +102,7 @@ python3 namecheap.py domains.ns.update --domain example.com --nameserver ns1.exa
|
||||
|
||||
## Behavior
|
||||
|
||||
- **Always check credentials first.** Before any API operation, verify `~/.namecheap-api` exists and is readable. If not, run the setup flow.
|
||||
- **Always check credentials first.** Before any API operation, verify `~/.namecheap-api` exists and is readable, and that `NAMECHEAP_API_KEY` is set in the current shell. If not, run the setup flow and have the user export the API key.
|
||||
- **Show current records before modifying.** Before adding or removing records, always fetch and display the current DNS records so the user can confirm the change.
|
||||
- **Use `ask_user` to confirm destructive changes.** Before removing records or replacing all records with `setHosts`, confirm with the user.
|
||||
- **The Namecheap `setHosts` API replaces ALL records.** Never call `domains.dns.setHosts` directly unless you have fetched all existing records first. Use `dns.addHost` and `dns.removeHost` for safe single-record operations — they handle the fetch-modify-write cycle internally.
|
||||
@@ -111,14 +111,13 @@ python3 namecheap.py domains.ns.update --domain example.com --nameserver ns1.exa
|
||||
|
||||
## Credential Storage
|
||||
|
||||
Credentials are stored in `~/.namecheap-api`:
|
||||
The Namecheap username is stored in `~/.namecheap-api`:
|
||||
|
||||
```bash
|
||||
NAMECHEAP_API_USER="username"
|
||||
NAMECHEAP_API_KEY="api-key-here"
|
||||
```
|
||||
|
||||
This file must have `600` permissions (owner read/write only). Alternatively, the script reads credentials from the `NAMECHEAP_API_USER` and `NAMECHEAP_API_KEY` environment variables, which take precedence over the file when both are set.
|
||||
This file must have `600` permissions (owner read/write only). The API key is **not** stored on disk; the script reads `NAMECHEAP_API_KEY` from the environment and uses it only in memory. `NAMECHEAP_API_USER` may be supplied either in the file or in the environment, and environment variables take precedence when both are set. If a legacy `NAMECHEAP_API_KEY` line already exists in `~/.namecheap-api`, it should be removed after the user exports the key securely in their shell.
|
||||
|
||||
## Supported Record Types
|
||||
|
||||
|
||||
+126
-50
@@ -1,9 +1,11 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Namecheap API CLI wrapper for DNS management.
|
||||
|
||||
Uses only the Python standard library (no third-party dependencies). Credentials
|
||||
are read from ``~/.namecheap-api`` (or env vars) and are never passed on the
|
||||
command line, so they cannot leak via ``ps``/shell history.
|
||||
Uses only the Python standard library (no third-party dependencies). The
|
||||
username can be cached in ``~/.namecheap-api`` while the API key is read from
|
||||
the ``NAMECHEAP_API_KEY`` environment variable (or prompted during setup) and
|
||||
is never passed on the command line, so it cannot leak via ``ps``/shell
|
||||
history.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
@@ -75,47 +77,107 @@ class NamecheapError(Exception):
|
||||
|
||||
# --- Configuration --------------------------------------------------------
|
||||
|
||||
def _redact_sensitive_text(text):
|
||||
text = re.sub(r'(?i)(ApiKey=)[^&\s]+', r"\1[REDACTED]", text)
|
||||
text = re.sub(r'(?i)(NAMECHEAP_API_KEY\s*=\s*"?)[^"\n]+("?)', r"\1[REDACTED]\2", text)
|
||||
return text
|
||||
|
||||
def _read_config_values():
|
||||
"""Return shell-style KEY="value" pairs from the local config file."""
|
||||
if not os.path.isfile(CONFIG_FILE):
|
||||
return {}
|
||||
|
||||
with open(CONFIG_FILE, "r", encoding="utf-8") as fh:
|
||||
content = fh.read()
|
||||
|
||||
pattern = re.compile(r'^\s*([A-Z_]+)\s*=\s*"?([^"\n]*)"?\s*$', re.MULTILINE)
|
||||
return {m.group(1): m.group(2) for m in pattern.finditer(content)}
|
||||
|
||||
|
||||
def load_config():
|
||||
"""Return (api_user, api_key), preferring env vars then the config file."""
|
||||
"""Return (api_user, api_key), preferring env vars for sensitive values."""
|
||||
api_user = os.environ.get("NAMECHEAP_API_USER")
|
||||
api_key = os.environ.get("NAMECHEAP_API_KEY")
|
||||
if api_user and api_key:
|
||||
return api_user, api_key
|
||||
|
||||
if os.path.isfile(CONFIG_FILE):
|
||||
with open(CONFIG_FILE, "r", encoding="utf-8") as fh:
|
||||
content = fh.read()
|
||||
# File uses shell-style KEY="value" lines for backward compatibility.
|
||||
pattern = re.compile(r'^\s*([A-Z_]+)\s*=\s*"?([^"\n]*)"?\s*$', re.MULTILINE)
|
||||
values = {m.group(1): m.group(2) for m in pattern.finditer(content)}
|
||||
api_user = api_user or values.get("NAMECHEAP_API_USER")
|
||||
api_key = api_key or values.get("NAMECHEAP_API_KEY")
|
||||
values = _read_config_values()
|
||||
api_user = api_user or values.get("NAMECHEAP_API_USER")
|
||||
|
||||
return api_user, api_key
|
||||
|
||||
|
||||
def has_legacy_api_key():
|
||||
"""Return True when a legacy clear-text API key is still present on disk."""
|
||||
return bool(_read_config_values().get("NAMECHEAP_API_KEY"))
|
||||
|
||||
|
||||
def save_config(api_user):
|
||||
with open(CONFIG_FILE, "w", encoding="utf-8") as fh:
|
||||
fh.write("# Cached Namecheap username for this skill.\n")
|
||||
fh.write("# Export NAMECHEAP_API_KEY in your shell before running API commands.\n")
|
||||
fh.write(f'NAMECHEAP_API_USER="{api_user}"\n')
|
||||
os.chmod(CONFIG_FILE, stat.S_IRUSR | stat.S_IWUSR) # 600
|
||||
|
||||
|
||||
def print_safe_api_failure(prefix, public_ip=None):
|
||||
err(prefix)
|
||||
print("Please verify:")
|
||||
print(" 1. API access is enabled (ON) at the Namecheap settings page")
|
||||
if public_ip:
|
||||
print(f" 2. IP address {public_ip} is whitelisted")
|
||||
else:
|
||||
print(" 2. Your current public IP is whitelisted")
|
||||
print(" 3. Your username and API key are correct")
|
||||
|
||||
|
||||
def print_safe_namecheap_error(exc):
|
||||
message = _redact_sensitive_text(str(exc)).strip()
|
||||
if message:
|
||||
err(f"API returned error: {message}")
|
||||
else:
|
||||
err("API returned an error.")
|
||||
|
||||
|
||||
def print_safe_runtime_error(exc):
|
||||
if isinstance(exc, urllib.error.URLError):
|
||||
err("Network error while contacting Namecheap or resolving your public IP.")
|
||||
return
|
||||
if isinstance(exc, OSError):
|
||||
err(f"Could not read or update {CONFIG_FILE}.")
|
||||
return
|
||||
if isinstance(exc, ET.ParseError):
|
||||
err("Received an invalid XML response from the Namecheap API.")
|
||||
return
|
||||
if isinstance(exc, json.JSONDecodeError):
|
||||
err("Could not parse the JSON input file that was provided.")
|
||||
return
|
||||
err("The operation failed.")
|
||||
|
||||
|
||||
def print_credential_help(legacy_key_present=False):
|
||||
err("Namecheap API credentials are not configured.")
|
||||
print()
|
||||
print("Run 'python3 namecheap.py setup' to save your Namecheap username.")
|
||||
print("Then export NAMECHEAP_API_KEY in your own shell before running API commands.")
|
||||
print()
|
||||
print("You need:")
|
||||
print(" 1. Your Namecheap username")
|
||||
print(" 2. An API key from: https://ap.www.namecheap.com/settings/tools/apiaccess/")
|
||||
print(" 3. Your public IP whitelisted in the API settings")
|
||||
if legacy_key_present:
|
||||
print()
|
||||
warn(f"A legacy API key entry in {CONFIG_FILE} is ignored for security reasons.")
|
||||
print("Remove the NAMECHEAP_API_KEY line from that file after exporting the key securely.")
|
||||
|
||||
|
||||
def check_credentials():
|
||||
api_user, api_key = load_config()
|
||||
legacy_key_present = has_legacy_api_key()
|
||||
if not api_user or not api_key:
|
||||
err("Namecheap API credentials not configured.")
|
||||
print()
|
||||
print("Run 'python3 namecheap.py setup' to configure your credentials.")
|
||||
print()
|
||||
print("You need:")
|
||||
print(" 1. Your Namecheap username")
|
||||
print(" 2. An API key from: https://ap.www.namecheap.com/settings/tools/apiaccess/")
|
||||
print(" 3. Your public IP whitelisted in the API settings")
|
||||
print_credential_help(legacy_key_present)
|
||||
sys.exit(1)
|
||||
return api_user, api_key
|
||||
|
||||
|
||||
def save_config(api_user, api_key):
|
||||
with open(CONFIG_FILE, "w", encoding="utf-8") as fh:
|
||||
fh.write(f'NAMECHEAP_API_USER="{api_user}"\n')
|
||||
fh.write(f'NAMECHEAP_API_KEY="{api_key}"\n')
|
||||
os.chmod(CONFIG_FILE, stat.S_IRUSR | stat.S_IWUSR) # 600
|
||||
|
||||
|
||||
# --- Networking -----------------------------------------------------------
|
||||
|
||||
def _http_get(url, timeout=15):
|
||||
@@ -224,23 +286,38 @@ def cmd_setup(_args):
|
||||
print()
|
||||
|
||||
existing_user, existing_key = load_config()
|
||||
if existing_user and existing_key:
|
||||
info(f"Existing configuration found for user: {existing_user}")
|
||||
print("\nTesting API connection...")
|
||||
try:
|
||||
api_request("domains.getList", {"PageSize": "1"})
|
||||
success("API connection successful!")
|
||||
except Exception as exc: # noqa: BLE001
|
||||
err(f"API connection failed: {exc}")
|
||||
if has_legacy_api_key():
|
||||
warn(f"A legacy API key was found in {CONFIG_FILE} and will no longer be used.")
|
||||
print("Run setup to rewrite the file without the API key, then export NAMECHEAP_API_KEY securely.")
|
||||
print()
|
||||
answer = input("Update stored credentials? [y/N]: ").strip().lower()
|
||||
|
||||
if existing_user:
|
||||
info(f"Existing configuration found for user: {existing_user}")
|
||||
if existing_key:
|
||||
print("\nTesting API connection...")
|
||||
try:
|
||||
api_request("domains.getList", {"PageSize": "1"})
|
||||
success("API connection successful!")
|
||||
except NamecheapError:
|
||||
print_safe_api_failure("API connection failed.", public_ip)
|
||||
except (urllib.error.URLError, OSError, ET.ParseError, json.JSONDecodeError) as exc:
|
||||
print_safe_runtime_error(exc)
|
||||
else:
|
||||
info("No NAMECHEAP_API_KEY environment variable is set yet.")
|
||||
print("The setup flow will validate the API key you enter, but it will not write the key to disk.")
|
||||
print()
|
||||
answer = input("Update saved username or validate a new API key? [y/N]: ").strip().lower()
|
||||
if answer not in ("y", "yes"):
|
||||
info("Keeping existing credentials.")
|
||||
return
|
||||
print()
|
||||
|
||||
print("Enter your Namecheap credentials:\n")
|
||||
api_user = input(" API Username: ").strip()
|
||||
prompt = " API Username"
|
||||
if existing_user:
|
||||
prompt += f" [{existing_user}]"
|
||||
prompt += ": "
|
||||
api_user = input(prompt).strip() or existing_user or ""
|
||||
api_key = getpass.getpass(" API Key (hidden): ").strip()
|
||||
print()
|
||||
|
||||
@@ -248,8 +325,9 @@ def cmd_setup(_args):
|
||||
err("Both username and API key are required.")
|
||||
sys.exit(1)
|
||||
|
||||
save_config(api_user, api_key)
|
||||
success(f"Credentials saved to {CONFIG_FILE}")
|
||||
save_config(api_user)
|
||||
success(f"Username saved to {CONFIG_FILE}")
|
||||
info("The API key was not written to disk. Export NAMECHEAP_API_KEY in your shell for future commands.")
|
||||
print("\nTesting API connection...")
|
||||
try:
|
||||
# Use the just-entered credentials directly for the validation call.
|
||||
@@ -257,12 +335,10 @@ def cmd_setup(_args):
|
||||
os.environ["NAMECHEAP_API_KEY"] = api_key
|
||||
api_request("domains.getList", {"PageSize": "1"})
|
||||
success("API connection successful!")
|
||||
except Exception as exc: # noqa: BLE001
|
||||
warn("API connection failed. Please verify:")
|
||||
print(" 1. API access is enabled (ON) at the Namecheap settings page")
|
||||
print(f" 2. IP address {public_ip} is whitelisted")
|
||||
print(" 3. Your API key is correct")
|
||||
print(f" (details: {exc})")
|
||||
except NamecheapError:
|
||||
print_safe_api_failure("API connection failed.", public_ip)
|
||||
except (urllib.error.URLError, OSError, ET.ParseError, json.JSONDecodeError) as exc:
|
||||
print_safe_runtime_error(exc)
|
||||
|
||||
|
||||
def cmd_domains_list(args):
|
||||
@@ -682,13 +758,13 @@ def main(argv=None):
|
||||
try:
|
||||
args.func(args)
|
||||
except NamecheapError as exc:
|
||||
err(f"API returned error: {exc}")
|
||||
print_safe_namecheap_error(exc)
|
||||
return 1
|
||||
except urllib.error.URLError as exc:
|
||||
err(f"Network error: {exc}")
|
||||
print_safe_runtime_error(exc)
|
||||
return 1
|
||||
except (OSError, ET.ParseError, json.JSONDecodeError) as exc:
|
||||
err(str(exc))
|
||||
print_safe_runtime_error(exc)
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
Reference in New Issue
Block a user