mirror of
https://github.com/github/awesome-copilot.git
synced 2026-07-17 19:31:20 +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:
|
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:
|
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
|
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
|
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.
|
d. **Confirm** — once the user reports setup succeeded, proceed with DNS operations.
|
||||||
|
|
||||||
### DNS Operations
|
### DNS Operations
|
||||||
@@ -102,7 +102,7 @@ python3 namecheap.py domains.ns.update --domain example.com --nameserver ns1.exa
|
|||||||
|
|
||||||
## Behavior
|
## 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.
|
- **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.
|
- **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.
|
- **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
|
## Credential Storage
|
||||||
|
|
||||||
Credentials are stored in `~/.namecheap-api`:
|
The Namecheap username is stored in `~/.namecheap-api`:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
NAMECHEAP_API_USER="username"
|
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
|
## Supported Record Types
|
||||||
|
|
||||||
|
|||||||
+126
-50
@@ -1,9 +1,11 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""Namecheap API CLI wrapper for DNS management.
|
"""Namecheap API CLI wrapper for DNS management.
|
||||||
|
|
||||||
Uses only the Python standard library (no third-party dependencies). Credentials
|
Uses only the Python standard library (no third-party dependencies). The
|
||||||
are read from ``~/.namecheap-api`` (or env vars) and are never passed on the
|
username can be cached in ``~/.namecheap-api`` while the API key is read from
|
||||||
command line, so they cannot leak via ``ps``/shell history.
|
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
|
import argparse
|
||||||
@@ -75,47 +77,107 @@ class NamecheapError(Exception):
|
|||||||
|
|
||||||
# --- Configuration --------------------------------------------------------
|
# --- 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():
|
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_user = os.environ.get("NAMECHEAP_API_USER")
|
||||||
api_key = os.environ.get("NAMECHEAP_API_KEY")
|
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):
|
values = _read_config_values()
|
||||||
with open(CONFIG_FILE, "r", encoding="utf-8") as fh:
|
api_user = api_user or values.get("NAMECHEAP_API_USER")
|
||||||
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")
|
|
||||||
|
|
||||||
return api_user, api_key
|
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():
|
def check_credentials():
|
||||||
api_user, api_key = load_config()
|
api_user, api_key = load_config()
|
||||||
|
legacy_key_present = has_legacy_api_key()
|
||||||
if not api_user or not api_key:
|
if not api_user or not api_key:
|
||||||
err("Namecheap API credentials not configured.")
|
print_credential_help(legacy_key_present)
|
||||||
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")
|
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
return api_user, api_key
|
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 -----------------------------------------------------------
|
# --- Networking -----------------------------------------------------------
|
||||||
|
|
||||||
def _http_get(url, timeout=15):
|
def _http_get(url, timeout=15):
|
||||||
@@ -224,23 +286,38 @@ def cmd_setup(_args):
|
|||||||
print()
|
print()
|
||||||
|
|
||||||
existing_user, existing_key = load_config()
|
existing_user, existing_key = load_config()
|
||||||
if existing_user and existing_key:
|
if has_legacy_api_key():
|
||||||
info(f"Existing configuration found for user: {existing_user}")
|
warn(f"A legacy API key was found in {CONFIG_FILE} and will no longer be used.")
|
||||||
print("\nTesting API connection...")
|
print("Run setup to rewrite the file without the API key, then export NAMECHEAP_API_KEY securely.")
|
||||||
try:
|
|
||||||
api_request("domains.getList", {"PageSize": "1"})
|
|
||||||
success("API connection successful!")
|
|
||||||
except Exception as exc: # noqa: BLE001
|
|
||||||
err(f"API connection failed: {exc}")
|
|
||||||
print()
|
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"):
|
if answer not in ("y", "yes"):
|
||||||
info("Keeping existing credentials.")
|
info("Keeping existing credentials.")
|
||||||
return
|
return
|
||||||
print()
|
print()
|
||||||
|
|
||||||
print("Enter your Namecheap credentials:\n")
|
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()
|
api_key = getpass.getpass(" API Key (hidden): ").strip()
|
||||||
print()
|
print()
|
||||||
|
|
||||||
@@ -248,8 +325,9 @@ def cmd_setup(_args):
|
|||||||
err("Both username and API key are required.")
|
err("Both username and API key are required.")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
save_config(api_user, api_key)
|
save_config(api_user)
|
||||||
success(f"Credentials saved to {CONFIG_FILE}")
|
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...")
|
print("\nTesting API connection...")
|
||||||
try:
|
try:
|
||||||
# Use the just-entered credentials directly for the validation call.
|
# 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
|
os.environ["NAMECHEAP_API_KEY"] = api_key
|
||||||
api_request("domains.getList", {"PageSize": "1"})
|
api_request("domains.getList", {"PageSize": "1"})
|
||||||
success("API connection successful!")
|
success("API connection successful!")
|
||||||
except Exception as exc: # noqa: BLE001
|
except NamecheapError:
|
||||||
warn("API connection failed. Please verify:")
|
print_safe_api_failure("API connection failed.", public_ip)
|
||||||
print(" 1. API access is enabled (ON) at the Namecheap settings page")
|
except (urllib.error.URLError, OSError, ET.ParseError, json.JSONDecodeError) as exc:
|
||||||
print(f" 2. IP address {public_ip} is whitelisted")
|
print_safe_runtime_error(exc)
|
||||||
print(" 3. Your API key is correct")
|
|
||||||
print(f" (details: {exc})")
|
|
||||||
|
|
||||||
|
|
||||||
def cmd_domains_list(args):
|
def cmd_domains_list(args):
|
||||||
@@ -682,13 +758,13 @@ def main(argv=None):
|
|||||||
try:
|
try:
|
||||||
args.func(args)
|
args.func(args)
|
||||||
except NamecheapError as exc:
|
except NamecheapError as exc:
|
||||||
err(f"API returned error: {exc}")
|
print_safe_namecheap_error(exc)
|
||||||
return 1
|
return 1
|
||||||
except urllib.error.URLError as exc:
|
except urllib.error.URLError as exc:
|
||||||
err(f"Network error: {exc}")
|
print_safe_runtime_error(exc)
|
||||||
return 1
|
return 1
|
||||||
except (OSError, ET.ParseError, json.JSONDecodeError) as exc:
|
except (OSError, ET.ParseError, json.JSONDecodeError) as exc:
|
||||||
err(str(exc))
|
print_safe_runtime_error(exc)
|
||||||
return 1
|
return 1
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user