mirror of
https://github.com/github/awesome-copilot.git
synced 2026-06-13 11:33:32 +00:00
Rewrite namecheap skill in Python to address PR review
Replace the Bash namecheap.sh with a stdlib-only Python CLI (namecheap.py) that resolves all Copilot PR review feedback: - Cache the public IP once per run instead of per request - Build API requests in-process via urllib so the API key never appears in process argv or shell history - Broaden multi-part TLD detection (co.uk, com.au, etc.) and document the limitation - Allow setup to update existing stored credentials - Stop soliciting the API key via chat; use terminal getpass or env vars - Remove non-portable Bash constructs (inline local, grep -oP) Also normalize domain casing, preserve MX priority 0, accept case-insensitive record types, and handle mixed-case email-forwarding attributes. Update SKILL.md and regenerate the skills README. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -245,7 +245,7 @@ See [CONTRIBUTING.md](../CONTRIBUTING.md#adding-skills) for guidelines on how to
|
||||
| [mvvm-toolkit](../skills/mvvm-toolkit/SKILL.md)<br />`gh skills install github/awesome-copilot mvvm-toolkit` | CommunityToolkit.Mvvm (the MVVM Toolkit) core: source generators ([ObservableProperty], [RelayCommand], [NotifyPropertyChangedFor], [NotifyCanExecuteChangedFor], [NotifyDataErrorInfo]), base classes (ObservableObject / ObservableValidator / ObservableRecipient), commands (RelayCommand / AsyncRelayCommand), and validation. Companion skills: mvvm-toolkit-messenger for pub/sub, mvvm-toolkit-di for Microsoft.Extensions.DependencyInjection wiring. Works across WPF, WinUI 3, MAUI, Uno, and Avalonia. | `references/end-to-end-walkthrough.md`<br />`references/relaycommand-cookbook.md`<br />`references/source-generators.md`<br />`references/troubleshooting.md`<br />`references/validation.md` |
|
||||
| [mvvm-toolkit-di](../skills/mvvm-toolkit-di/SKILL.md)<br />`gh skills install github/awesome-copilot mvvm-toolkit-di` | Wire CommunityToolkit.Mvvm ViewModels into Microsoft.Extensions.DependencyInjection. Covers the .NET Generic Host composition root, constructor injection, service lifetimes (Singleton / Transient / Scoped), IMessenger registration, resolving ViewModels in Views, keyed services, testing seams, and the legacy Ioc.Default escape hatch. Use across WPF, WinUI 3, .NET MAUI, Uno, and Avalonia. | `references/dependency-injection.md` |
|
||||
| [mvvm-toolkit-messenger](../skills/mvvm-toolkit-messenger/SKILL.md)<br />`gh skills install github/awesome-copilot mvvm-toolkit-messenger` | CommunityToolkit.Mvvm Messenger pub/sub for decoupled communication between ViewModels (or any objects). Covers WeakReferenceMessenger vs StrongReferenceMessenger, IRecipient<TMessage>, RequestMessage<T> / AsyncRequestMessage<T> / CollectionRequestMessage<T>, ValueChangedMessage<T>, channels (tokens), and the ObservableRecipient activation lifecycle. Use across WPF, WinUI 3, .NET MAUI, Uno, and Avalonia. | `references/messenger-patterns.md` |
|
||||
| [namecheap](../skills/namecheap/SKILL.md)<br />`gh skills install github/awesome-copilot namecheap` | Manage DNS records for domains registered with Namecheap via their API. List domains, view/add/update/remove DNS host entries (A, AAAA, CNAME, MX, TXT, etc.), and guide users through API setup including public IP detection and credential configuration. Use when the user mentions Namecheap, DNS records, domain management, or wants to add/change/remove A records, CNAME records, MX records, or TXT records for their domains. | `namecheap.sh`<br />`references/namecheap-api.md` |
|
||||
| [namecheap](../skills/namecheap/SKILL.md)<br />`gh skills install github/awesome-copilot namecheap` | Manage DNS records for domains registered with Namecheap via their API. List domains, view/add/update/remove DNS host entries (A, AAAA, CNAME, MX, TXT, etc.), and guide users through API setup including public IP detection and credential configuration. Use when the user mentions Namecheap, DNS records, domain management, or wants to add/change/remove A records, CNAME records, MX records, or TXT records for their domains. | `namecheap.py`<br />`references/namecheap-api.md` |
|
||||
| [nano-banana-pro-openrouter](../skills/nano-banana-pro-openrouter/SKILL.md)<br />`gh skills install github/awesome-copilot nano-banana-pro-openrouter` | Generate or edit images via OpenRouter with the Gemini 3 Pro Image model. Use for prompt-only image generation, image edits, and multi-image compositing; supports 1K/2K/4K output. | `assets/SYSTEM_TEMPLATE`<br />`scripts/generate_image.py` |
|
||||
| [napkin](../skills/napkin/SKILL.md)<br />`gh skills install github/awesome-copilot napkin` | Visual whiteboard collaboration for Copilot CLI. Creates an interactive whiteboard that opens in your browser — draw, sketch, add sticky notes, then share everything back with Copilot. Copilot sees your drawings and text, and responds with analysis, suggestions, and ideas. | `assets/napkin.html`<br />`assets/step1-activate.svg`<br />`assets/step2-whiteboard.svg`<br />`assets/step3-draw.svg`<br />`assets/step4-share.svg`<br />`assets/step5-response.svg` |
|
||||
| [next-intl-add-language](../skills/next-intl-add-language/SKILL.md)<br />`gh skills install github/awesome-copilot next-intl-add-language` | Add new language to a Next.js + next-intl application | None |
|
||||
|
||||
+23
-24
@@ -17,67 +17,66 @@ Before executing any API commands, verify credentials are configured:
|
||||
|
||||
1. **Check for existing config** — look for `~/.namecheap-api`
|
||||
2. If not configured, guide the user through setup:
|
||||
a. **Show public IP** — run `curl -s https://api.ipify.org` 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
|
||||
c. **Collect credentials** — use `ask_user` to get their Namecheap username, then their API key
|
||||
d. **Save config** — write credentials to `~/.namecheap-api` with `chmod 600`
|
||||
e. **Validate** — run a test API call to confirm access works
|
||||
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`.
|
||||
d. **Confirm** — once the user reports setup succeeded, proceed with DNS operations.
|
||||
|
||||
### DNS Operations
|
||||
|
||||
Use the `namecheap.sh` script (bundled in this skill's directory) for all API interactions:
|
||||
Use the `namecheap.py` script (bundled in this skill's directory) for all API interactions. It requires only Python 3 (standard library only — no `pip install` needed) and works the same on macOS, Linux, and Windows:
|
||||
|
||||
```bash
|
||||
# Show public IP (for setup)
|
||||
bash namecheap.sh public-ip
|
||||
python3 namecheap.py public-ip
|
||||
|
||||
# Run setup flow
|
||||
bash namecheap.sh setup
|
||||
python3 namecheap.py setup
|
||||
|
||||
# List domains
|
||||
bash namecheap.sh domains.getList
|
||||
python3 namecheap.py domains.getList
|
||||
|
||||
# Get nameservers for a domain (shows if using Namecheap DNS or custom)
|
||||
bash namecheap.sh domains.dns.getList --domain example.com
|
||||
python3 namecheap.py domains.dns.getList --domain example.com
|
||||
|
||||
# Get DNS records for a domain
|
||||
bash namecheap.sh domains.dns.getHosts --domain example.com
|
||||
python3 namecheap.py domains.dns.getHosts --domain example.com
|
||||
|
||||
# Add a single record (preserves existing records)
|
||||
bash namecheap.sh dns.addHost --domain example.com --type A --name www --address 1.2.3.4 --ttl 1800
|
||||
python3 namecheap.py dns.addHost --domain example.com --type A --name www --address 1.2.3.4 --ttl 1800
|
||||
|
||||
# Remove a single record
|
||||
bash namecheap.sh dns.removeHost --domain example.com --type A --name www --address 1.2.3.4
|
||||
python3 namecheap.py dns.removeHost --domain example.com --type A --name www --address 1.2.3.4
|
||||
|
||||
# Replace all records from a JSON file
|
||||
bash namecheap.sh domains.dns.setHosts --domain example.com --hosts records.json
|
||||
python3 namecheap.py domains.dns.setHosts --domain example.com --hosts records.json
|
||||
|
||||
# Switch to Namecheap default DNS
|
||||
bash namecheap.sh domains.dns.setDefault --domain example.com
|
||||
python3 namecheap.py domains.dns.setDefault --domain example.com
|
||||
|
||||
# Switch to custom nameservers
|
||||
bash namecheap.sh domains.dns.setCustom --domain example.com --nameservers ns1.cloudflare.com,ns2.cloudflare.com
|
||||
python3 namecheap.py domains.dns.setCustom --domain example.com --nameservers ns1.cloudflare.com,ns2.cloudflare.com
|
||||
|
||||
# Get email forwarding rules
|
||||
bash namecheap.sh domains.dns.getEmailForwarding --domain example.com
|
||||
python3 namecheap.py domains.dns.getEmailForwarding --domain example.com
|
||||
|
||||
# Set email forwarding (single rule)
|
||||
bash namecheap.sh domains.dns.setEmailForwarding --domain example.com --mailbox info --forward-to user@gmail.com
|
||||
python3 namecheap.py domains.dns.setEmailForwarding --domain example.com --mailbox info --forward-to user@gmail.com
|
||||
|
||||
# Set email forwarding (from JSON file)
|
||||
bash namecheap.sh domains.dns.setEmailForwarding --domain example.com --forwards forwards.json
|
||||
python3 namecheap.py domains.dns.setEmailForwarding --domain example.com --forwards forwards.json
|
||||
|
||||
# Create a child nameserver (glue record)
|
||||
bash namecheap.sh domains.ns.create --domain example.com --nameserver ns1.example.com --ip 1.2.3.4
|
||||
python3 namecheap.py domains.ns.create --domain example.com --nameserver ns1.example.com --ip 1.2.3.4
|
||||
|
||||
# Delete a child nameserver
|
||||
bash namecheap.sh domains.ns.delete --domain example.com --nameserver ns1.example.com
|
||||
python3 namecheap.py domains.ns.delete --domain example.com --nameserver ns1.example.com
|
||||
|
||||
# Get nameserver info
|
||||
bash namecheap.sh domains.ns.getInfo --domain example.com --nameserver ns1.example.com
|
||||
python3 namecheap.py domains.ns.getInfo --domain example.com --nameserver ns1.example.com
|
||||
|
||||
# Update nameserver IP
|
||||
bash namecheap.sh domains.ns.update --domain example.com --nameserver ns1.example.com --old-ip 1.2.3.4 --ip 5.6.7.8
|
||||
python3 namecheap.py domains.ns.update --domain example.com --nameserver ns1.example.com --old-ip 1.2.3.4 --ip 5.6.7.8
|
||||
```
|
||||
|
||||
## Behavior
|
||||
@@ -87,7 +86,7 @@ bash namecheap.sh domains.ns.update --domain example.com --nameserver ns1.exampl
|
||||
- **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.
|
||||
- **Explain TTL in human terms.** When the user asks about TTL, explain that 1800 = 30 minutes, 3600 = 1 hour, etc.
|
||||
- **Handle multi-part TLDs.** Domains like `example.co.uk` have SLD=example and TLD=co.uk. The script handles this automatically.
|
||||
- **Handle multi-part TLDs.** Domains like `example.co.uk` have SLD=example and TLD=co.uk. The script recognizes a built-in list of common second-level suffixes (e.g. `co.uk`, `com.au`, `co.jp`, `com.br`). This list is best-effort and not a full public-suffix database — if a domain with an unlisted multi-part suffix returns a `2019166` ("Domain not found") error, the SLD/TLD split was likely wrong. In that case, confirm the registered domain with the user and report the limitation.
|
||||
|
||||
## Credential Storage
|
||||
|
||||
@@ -98,7 +97,7 @@ NAMECHEAP_API_USER="username"
|
||||
NAMECHEAP_API_KEY="api-key-here"
|
||||
```
|
||||
|
||||
This file must have `600` permissions (owner read/write only).
|
||||
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.
|
||||
|
||||
## Supported Record Types
|
||||
|
||||
|
||||
@@ -0,0 +1,697 @@
|
||||
#!/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.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import getpass
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import stat
|
||||
import sys
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
API_URL = "https://api.namecheap.com/xml.response"
|
||||
CONFIG_FILE = os.path.join(os.path.expanduser("~"), ".namecheap-api")
|
||||
|
||||
# Known multi-part (second-level) public suffixes. Best-effort list, not a full
|
||||
# public-suffix database. For unlisted suffixes the domain is split on the last
|
||||
# dot, which is correct for single-label TLDs (.com, .io, .dev, ...).
|
||||
MULTI_PART_SUFFIXES = {
|
||||
"co.uk", "org.uk", "me.uk", "ac.uk", "gov.uk", "net.uk", "ltd.uk", "plc.uk",
|
||||
"com.au", "net.au", "org.au", "id.au",
|
||||
"co.nz", "net.nz", "org.nz",
|
||||
"co.za", "org.za",
|
||||
"co.jp", "ne.jp", "or.jp", "ac.jp", "go.jp",
|
||||
"co.kr", "or.kr", "ne.kr",
|
||||
"com.br", "net.br", "org.br",
|
||||
"com.cn", "net.cn", "org.cn",
|
||||
"co.in", "net.in", "org.in",
|
||||
"com.mx", "org.mx",
|
||||
"com.sg", "edu.sg",
|
||||
"com.tr",
|
||||
"co.il", "org.il",
|
||||
}
|
||||
|
||||
# Cached public IP for the lifetime of the process.
|
||||
_public_ip = None
|
||||
|
||||
|
||||
# --- Output helpers -------------------------------------------------------
|
||||
|
||||
_USE_COLOR = sys.stdout.isatty()
|
||||
|
||||
|
||||
def _c(code, text):
|
||||
return f"\033[{code}m{text}\033[0m" if _USE_COLOR else text
|
||||
|
||||
|
||||
def err(msg):
|
||||
print(_c("0;31", "Error:") + " " + msg, file=sys.stderr)
|
||||
|
||||
|
||||
def success(msg):
|
||||
print(_c("0;32", "\u2713") + " " + msg)
|
||||
|
||||
|
||||
def info(msg):
|
||||
print(_c("0;36", "\u2139") + " " + msg)
|
||||
|
||||
|
||||
def warn(msg):
|
||||
print(_c("1;33", "\u26a0") + " " + msg)
|
||||
|
||||
|
||||
class NamecheapError(Exception):
|
||||
"""Raised when the API returns a Status="ERROR" response."""
|
||||
|
||||
|
||||
# --- Configuration --------------------------------------------------------
|
||||
|
||||
def load_config():
|
||||
"""Return (api_user, api_key), preferring env vars then the config file."""
|
||||
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")
|
||||
|
||||
return api_user, api_key
|
||||
|
||||
|
||||
def check_credentials():
|
||||
api_user, api_key = load_config()
|
||||
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")
|
||||
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):
|
||||
req = urllib.request.Request(url, headers={"User-Agent": "namecheap-skill/1.0"})
|
||||
with urllib.request.urlopen(req, timeout=timeout) as resp: # noqa: S310 (https only)
|
||||
return resp.read().decode("utf-8", errors="replace")
|
||||
|
||||
|
||||
def get_public_ip():
|
||||
"""Resolve the public IP once and cache it for subsequent calls."""
|
||||
global _public_ip
|
||||
if _public_ip:
|
||||
return _public_ip
|
||||
for url in ("https://api.ipify.org", "https://ifconfig.me"):
|
||||
try:
|
||||
ip = _http_get(url, timeout=10).strip()
|
||||
if ip:
|
||||
_public_ip = ip
|
||||
return ip
|
||||
except Exception:
|
||||
continue
|
||||
_public_ip = "unknown"
|
||||
return _public_ip
|
||||
|
||||
|
||||
def _strip_namespaces(root):
|
||||
for elem in root.iter():
|
||||
if isinstance(elem.tag, str) and "}" in elem.tag:
|
||||
elem.tag = elem.tag.split("}", 1)[1]
|
||||
return root
|
||||
|
||||
|
||||
def _check_error(root):
|
||||
if (root.attrib.get("Status") or "").upper() == "ERROR":
|
||||
messages = []
|
||||
for e in root.iter("Err"):
|
||||
code = e.attrib.get("Number") or e.attrib.get("Code") or ""
|
||||
text = (e.text or "").strip()
|
||||
messages.append(f"[{code}] {text}" if code else text)
|
||||
raise NamecheapError("; ".join(m for m in messages if m) or "Unknown API error")
|
||||
|
||||
|
||||
def api_request(command, params=None):
|
||||
"""Issue a Namecheap API GET request and return the parsed (ns-stripped) root.
|
||||
|
||||
The API key is encoded into the request URL inside this process; it is never
|
||||
passed as a command-line argument, so it cannot leak via ``ps`` or shell
|
||||
history. Values are URL-encoded by ``urllib``.
|
||||
"""
|
||||
api_user, api_key = check_credentials()
|
||||
query = {
|
||||
"ApiUser": api_user,
|
||||
"ApiKey": api_key,
|
||||
"UserName": api_user,
|
||||
"Command": f"namecheap.{command}",
|
||||
"ClientIp": get_public_ip(),
|
||||
}
|
||||
for key, value in (params or {}).items():
|
||||
if value is not None and value != "":
|
||||
query[key] = value
|
||||
|
||||
url = f"{API_URL}?{urllib.parse.urlencode(query)}"
|
||||
body = _http_get(url, timeout=30)
|
||||
root = _strip_namespaces(ET.fromstring(body))
|
||||
_check_error(root)
|
||||
return root
|
||||
|
||||
|
||||
def _attr(root, tag, name, default=""):
|
||||
for elem in root.iter(tag):
|
||||
return elem.attrib.get(name, default)
|
||||
return default
|
||||
|
||||
|
||||
# --- Domain parsing -------------------------------------------------------
|
||||
|
||||
def parse_domain(domain):
|
||||
"""Split a registered domain into (SLD, TLD), handling multi-part TLDs."""
|
||||
domain = domain.strip().rstrip(".").lower()
|
||||
labels = domain.split(".")
|
||||
if len(labels) >= 3 and ".".join(labels[-2:]) in MULTI_PART_SUFFIXES:
|
||||
tld = ".".join(labels[-2:])
|
||||
sld = ".".join(labels[:-2])
|
||||
elif len(labels) >= 2:
|
||||
tld = labels[-1]
|
||||
sld = ".".join(labels[:-1])
|
||||
else:
|
||||
tld = ""
|
||||
sld = domain
|
||||
return sld, tld
|
||||
|
||||
|
||||
# --- Commands -------------------------------------------------------------
|
||||
|
||||
def cmd_public_ip(_args):
|
||||
print(get_public_ip())
|
||||
|
||||
|
||||
def cmd_setup(_args):
|
||||
print("=== Namecheap API Setup ===\n")
|
||||
public_ip = get_public_ip()
|
||||
info("Your public IP address is: " + _c("0;36", public_ip))
|
||||
print()
|
||||
print("Make sure this IP is whitelisted at:")
|
||||
print(" https://ap.www.namecheap.com/settings/tools/apiaccess/")
|
||||
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}")
|
||||
print()
|
||||
answer = input("Update stored credentials? [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()
|
||||
api_key = getpass.getpass(" API Key (hidden): ").strip()
|
||||
print()
|
||||
|
||||
if not api_user or not api_key:
|
||||
err("Both username and API key are required.")
|
||||
sys.exit(1)
|
||||
|
||||
save_config(api_user, api_key)
|
||||
success(f"Credentials saved to {CONFIG_FILE}")
|
||||
print("\nTesting API connection...")
|
||||
try:
|
||||
# Use the just-entered credentials directly for the validation call.
|
||||
os.environ["NAMECHEAP_API_USER"] = api_user
|
||||
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})")
|
||||
|
||||
|
||||
def cmd_domains_list(args):
|
||||
params = {"ListType": args.type, "Page": str(args.page), "PageSize": str(args.page_size)}
|
||||
if args.search:
|
||||
params["SearchTerm"] = args.search
|
||||
info("Fetching domain list...")
|
||||
root = api_request("domains.getList", params)
|
||||
|
||||
print()
|
||||
print(f"{'DOMAIN':<30} {'EXPIRES':<12} {'LOCKED':<12} {'AUTO-RENEW':<10}")
|
||||
print(f"{'------':<30} {'-------':<12} {'------':<12} {'----------':<10}")
|
||||
for d in root.iter("Domain"):
|
||||
print("{:<30} {:<12} {:<12} {:<10}".format(
|
||||
d.attrib.get("Name", ""),
|
||||
d.attrib.get("Expires", ""),
|
||||
d.attrib.get("IsLocked", ""),
|
||||
d.attrib.get("AutoRenew", ""),
|
||||
))
|
||||
print()
|
||||
|
||||
|
||||
def _print_hosts(root):
|
||||
print()
|
||||
print(f"{'HOST':<20} {'TYPE':<8} {'ADDRESS':<40} {'TTL':<8} {'MXPREF':<6}")
|
||||
print(f"{'----':<20} {'----':<8} {'-------':<40} {'---':<8} {'------':<6}")
|
||||
for h in root.iter("host"):
|
||||
print("{:<20} {:<8} {:<40} {:<8} {:<6}".format(
|
||||
h.attrib.get("Name", ""),
|
||||
h.attrib.get("Type", ""),
|
||||
h.attrib.get("Address", ""),
|
||||
h.attrib.get("TTL", "1800"),
|
||||
h.attrib.get("MXPref", "-"),
|
||||
))
|
||||
print()
|
||||
|
||||
|
||||
def cmd_dns_get_hosts(args):
|
||||
sld, tld = parse_domain(args.domain)
|
||||
info(f"Fetching DNS records for {args.domain} (SLD={sld}, TLD={tld})...")
|
||||
root = api_request("domains.dns.getHosts", {"SLD": sld, "TLD": tld})
|
||||
_print_hosts(root)
|
||||
|
||||
|
||||
def _existing_hosts(root):
|
||||
"""Return existing host records as a list of dicts."""
|
||||
records = []
|
||||
for h in root.iter("host"):
|
||||
records.append({
|
||||
"name": h.attrib.get("Name", ""),
|
||||
"type": h.attrib.get("Type", ""),
|
||||
"address": h.attrib.get("Address", ""),
|
||||
"ttl": h.attrib.get("TTL", "1800"),
|
||||
"mxpref": h.attrib.get("MXPref", ""),
|
||||
})
|
||||
return records
|
||||
|
||||
|
||||
def _hosts_to_params(sld, tld, records):
|
||||
params = {"SLD": sld, "TLD": tld}
|
||||
i = 1
|
||||
for r in records:
|
||||
if not (r["name"] and r["type"] and r["address"]):
|
||||
continue
|
||||
params[f"HostName{i}"] = r["name"]
|
||||
params[f"RecordType{i}"] = r["type"]
|
||||
params[f"Address{i}"] = r["address"]
|
||||
params[f"TTL{i}"] = r.get("ttl") or "1800"
|
||||
mxpref = r.get("mxpref")
|
||||
if mxpref not in (None, ""):
|
||||
# MX priority 0 is valid, so always send MXPref for MX records;
|
||||
# for other record types only forward a non-zero value.
|
||||
if r["type"].upper() == "MX" or mxpref != "0":
|
||||
params[f"MXPref{i}"] = mxpref
|
||||
i += 1
|
||||
return params, i - 1
|
||||
|
||||
|
||||
def cmd_dns_set_hosts(args):
|
||||
if not os.path.isfile(args.hosts):
|
||||
err(f"Hosts file not found: {args.hosts}")
|
||||
sys.exit(1)
|
||||
with open(args.hosts, "r", encoding="utf-8") as fh:
|
||||
raw = json.load(fh)
|
||||
|
||||
records = []
|
||||
for r in raw:
|
||||
records.append({
|
||||
"name": r.get("HostName", ""),
|
||||
"type": r.get("RecordType", ""),
|
||||
"address": r.get("Address", ""),
|
||||
"ttl": str(r.get("TTL", "") or ""),
|
||||
"mxpref": str(r.get("MXPref", "") or ""),
|
||||
})
|
||||
|
||||
sld, tld = parse_domain(args.domain)
|
||||
params, count = _hosts_to_params(sld, tld, records)
|
||||
if count == 0:
|
||||
err(f"No valid host records found in {args.hosts}")
|
||||
sys.exit(1)
|
||||
|
||||
info(f"Setting {count} DNS records for {args.domain}...")
|
||||
root = api_request("domains.dns.setHosts", params)
|
||||
if _attr(root, "DomainDNSSetHostsResult", "IsSuccess").lower() == "true":
|
||||
success(f"DNS records updated successfully for {args.domain}!")
|
||||
else:
|
||||
err("Failed to update DNS records.")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def cmd_dns_add_host(args):
|
||||
sld, tld = parse_domain(args.domain)
|
||||
info(f"Fetching existing DNS records for {args.domain}...")
|
||||
root = api_request("domains.dns.getHosts", {"SLD": sld, "TLD": tld})
|
||||
records = _existing_hosts(root)
|
||||
records.append({
|
||||
"name": args.name,
|
||||
"type": args.type.upper(),
|
||||
"address": args.address,
|
||||
"ttl": args.ttl,
|
||||
"mxpref": args.mxpref or "",
|
||||
})
|
||||
params, _ = _hosts_to_params(sld, tld, records)
|
||||
|
||||
info(f"Adding {args.type.upper()} record: {args.name} -> {args.address}")
|
||||
result = api_request("domains.dns.setHosts", params)
|
||||
if _attr(result, "DomainDNSSetHostsResult", "IsSuccess").lower() == "true":
|
||||
success(f"DNS record added: {args.name} {args.type} {args.address}")
|
||||
else:
|
||||
err("Failed to add DNS record.")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def cmd_dns_remove_host(args):
|
||||
sld, tld = parse_domain(args.domain)
|
||||
info(f"Fetching existing DNS records for {args.domain}...")
|
||||
root = api_request("domains.dns.getHosts", {"SLD": sld, "TLD": tld})
|
||||
|
||||
kept = []
|
||||
removed = False
|
||||
for r in _existing_hosts(root):
|
||||
if (not removed and r["name"] == args.name
|
||||
and r["type"].upper() == args.type.upper()
|
||||
and (not args.address or r["address"] == args.address)):
|
||||
removed = True
|
||||
info(f"Removing record: {r['name']} {r['type']} {r['address']}")
|
||||
continue
|
||||
kept.append(r)
|
||||
|
||||
if not removed:
|
||||
err("No matching record found to remove.")
|
||||
sys.exit(1)
|
||||
|
||||
params, count = _hosts_to_params(sld, tld, kept)
|
||||
if count == 0:
|
||||
err("Cannot remove the last DNS record. Namecheap requires at least one record.")
|
||||
sys.exit(1)
|
||||
|
||||
info(f"Updating DNS records for {args.domain}...")
|
||||
result = api_request("domains.dns.setHosts", params)
|
||||
if _attr(result, "DomainDNSSetHostsResult", "IsSuccess").lower() == "true":
|
||||
success("DNS record removed successfully!")
|
||||
else:
|
||||
err("Failed to remove DNS record.")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def cmd_dns_get_list(args):
|
||||
sld, tld = parse_domain(args.domain)
|
||||
info(f"Fetching nameservers for {args.domain}...")
|
||||
root = api_request("domains.dns.getList", {"SLD": sld, "TLD": tld})
|
||||
|
||||
using = _attr(root, "DomainDNSGetListResult", "IsUsingOurDNS", "unknown")
|
||||
print()
|
||||
info(f"Using Namecheap DNS: {using}")
|
||||
print("\nNameservers:")
|
||||
for ns in root.iter("Nameserver"):
|
||||
if ns.text:
|
||||
print(f" - {ns.text.strip()}")
|
||||
print()
|
||||
|
||||
|
||||
def cmd_dns_set_default(args):
|
||||
sld, tld = parse_domain(args.domain)
|
||||
info(f"Setting {args.domain} to use Namecheap default DNS...")
|
||||
root = api_request("domains.dns.setDefault", {"SLD": sld, "TLD": tld})
|
||||
if _attr(root, "DomainDNSSetDefaultResult", "Updated").lower() == "true":
|
||||
success(f"Domain {args.domain} now uses Namecheap default DNS!")
|
||||
else:
|
||||
err("Failed to set default DNS.")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def cmd_dns_set_custom(args):
|
||||
sld, tld = parse_domain(args.domain)
|
||||
info(f"Setting {args.domain} to use custom nameservers: {args.nameservers}")
|
||||
root = api_request(
|
||||
"domains.dns.setCustom",
|
||||
{"SLD": sld, "TLD": tld, "Nameservers": args.nameservers},
|
||||
)
|
||||
if _attr(root, "DomainDNSSetCustomResult", "Updated").lower() == "true":
|
||||
success(f"Domain {args.domain} now uses custom nameservers!")
|
||||
else:
|
||||
err("Failed to set custom nameservers.")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def cmd_dns_get_email_forwarding(args):
|
||||
info(f"Fetching email forwarding for {args.domain}...")
|
||||
root = api_request("domains.dns.getEmailForwarding", {"DomainName": args.domain})
|
||||
|
||||
print()
|
||||
print(f"{'MAILBOX':<20} {'FORWARDS TO':<40}")
|
||||
print(f"{'-------':<20} {'-----------':<40}")
|
||||
for fwd in root.iter("Forward"):
|
||||
mailbox = (fwd.attrib.get("mailbox") or fwd.attrib.get("MailBox")
|
||||
or fwd.attrib.get("Mailbox") or "")
|
||||
forward_to = (fwd.attrib.get("ForwardTo") or fwd.attrib.get("forwardto")
|
||||
or (fwd.text or "").strip())
|
||||
print(f"{mailbox + '@' + args.domain:<20} {forward_to:<40}")
|
||||
print()
|
||||
|
||||
|
||||
def cmd_dns_set_email_forwarding(args):
|
||||
params = {"DomainName": args.domain}
|
||||
|
||||
if args.mailbox and args.forward_to:
|
||||
params["MailBox1"] = args.mailbox
|
||||
params["ForwardTo1"] = args.forward_to
|
||||
elif args.forwards:
|
||||
if not os.path.isfile(args.forwards):
|
||||
err(f"Forwards file not found: {args.forwards}")
|
||||
sys.exit(1)
|
||||
with open(args.forwards, "r", encoding="utf-8") as fh:
|
||||
rules = json.load(fh)
|
||||
i = 1
|
||||
for rule in rules:
|
||||
mailbox = rule.get("MailBox") or rule.get("mailbox") or ""
|
||||
forward_to = rule.get("ForwardTo") or rule.get("forwardto") or ""
|
||||
if mailbox and forward_to:
|
||||
params[f"MailBox{i}"] = mailbox
|
||||
params[f"ForwardTo{i}"] = forward_to
|
||||
i += 1
|
||||
else:
|
||||
err("Provide either --mailbox/--forward-to or --forwards <file.json>")
|
||||
sys.exit(1)
|
||||
|
||||
info(f"Setting email forwarding for {args.domain}...")
|
||||
root = api_request("domains.dns.setEmailForwarding", params)
|
||||
if _attr(root, "DomainDNSSetEmailForwardingResult", "IsSuccess").lower() == "true":
|
||||
success(f"Email forwarding updated for {args.domain}!")
|
||||
else:
|
||||
err("Failed to set email forwarding.")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def cmd_ns_create(args):
|
||||
sld, tld = parse_domain(args.domain)
|
||||
info(f"Creating nameserver {args.nameserver} -> {args.ip}...")
|
||||
root = api_request(
|
||||
"domains.ns.create",
|
||||
{"SLD": sld, "TLD": tld, "Nameserver": args.nameserver, "IP": args.ip},
|
||||
)
|
||||
if _attr(root, "DomainNSCreateResult", "IsSuccess").lower() == "true":
|
||||
success(f"Nameserver {args.nameserver} created!")
|
||||
else:
|
||||
err("Failed to create nameserver.")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def cmd_ns_delete(args):
|
||||
sld, tld = parse_domain(args.domain)
|
||||
info(f"Deleting nameserver {args.nameserver}...")
|
||||
root = api_request(
|
||||
"domains.ns.delete",
|
||||
{"SLD": sld, "TLD": tld, "Nameserver": args.nameserver},
|
||||
)
|
||||
if _attr(root, "DomainNSDeleteResult", "IsSuccess").lower() == "true":
|
||||
success(f"Nameserver {args.nameserver} deleted!")
|
||||
else:
|
||||
err("Failed to delete nameserver.")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def cmd_ns_get_info(args):
|
||||
sld, tld = parse_domain(args.domain)
|
||||
info(f"Fetching info for nameserver {args.nameserver}...")
|
||||
root = api_request(
|
||||
"domains.ns.getInfo",
|
||||
{"SLD": sld, "TLD": tld, "Nameserver": args.nameserver},
|
||||
)
|
||||
ns_ip = _attr(root, "DomainNSInfoResult", "IP", "unknown")
|
||||
print()
|
||||
print(f"Nameserver: {args.nameserver}")
|
||||
print(f"IP Address: {ns_ip}")
|
||||
statuses = [s.text.strip() for s in root.iter("Status") if s.text and s.text.strip()]
|
||||
if statuses:
|
||||
print(f"Status: {', '.join(statuses)}")
|
||||
print()
|
||||
|
||||
|
||||
def cmd_ns_update(args):
|
||||
sld, tld = parse_domain(args.domain)
|
||||
info(f"Updating nameserver {args.nameserver}: {args.old_ip} -> {args.ip}...")
|
||||
root = api_request(
|
||||
"domains.ns.update",
|
||||
{"SLD": sld, "TLD": tld, "Nameserver": args.nameserver,
|
||||
"OldIP": args.old_ip, "IP": args.ip},
|
||||
)
|
||||
if _attr(root, "DomainNSUpdateResult", "IsSuccess").lower() == "true":
|
||||
success(f"Nameserver {args.nameserver} updated to {args.ip}!")
|
||||
else:
|
||||
err("Failed to update nameserver.")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
# --- Argument parsing -----------------------------------------------------
|
||||
|
||||
def build_parser():
|
||||
parser = argparse.ArgumentParser(
|
||||
prog="namecheap.py",
|
||||
description="Namecheap DNS Management CLI",
|
||||
)
|
||||
sub = parser.add_subparsers(dest="command", metavar="<command>")
|
||||
|
||||
sub.add_parser("setup", help="Configure API credentials and test connection").set_defaults(func=cmd_setup)
|
||||
sub.add_parser("public-ip", help="Show your public IP address").set_defaults(func=cmd_public_ip)
|
||||
|
||||
p = sub.add_parser("domains.getList", help="List your Namecheap domains")
|
||||
p.add_argument("--type", default="ALL")
|
||||
p.add_argument("--search", default="")
|
||||
p.add_argument("--page", type=int, default=1)
|
||||
p.add_argument("--page-size", type=int, default=20)
|
||||
p.set_defaults(func=cmd_domains_list)
|
||||
|
||||
p = sub.add_parser("domains.dns.getList", help="Get nameservers for a domain")
|
||||
p.add_argument("--domain", required=True)
|
||||
p.set_defaults(func=cmd_dns_get_list)
|
||||
|
||||
p = sub.add_parser("domains.dns.getHosts", help="Get DNS records for a domain")
|
||||
p.add_argument("--domain", required=True)
|
||||
p.set_defaults(func=cmd_dns_get_hosts)
|
||||
|
||||
p = sub.add_parser("domains.dns.setHosts", help="Set all DNS records (from JSON file)")
|
||||
p.add_argument("--domain", required=True)
|
||||
p.add_argument("--hosts", required=True)
|
||||
p.set_defaults(func=cmd_dns_set_hosts)
|
||||
|
||||
p = sub.add_parser("domains.dns.setDefault", help="Use Namecheap default DNS")
|
||||
p.add_argument("--domain", required=True)
|
||||
p.set_defaults(func=cmd_dns_set_default)
|
||||
|
||||
p = sub.add_parser("domains.dns.setCustom", help="Use custom nameservers")
|
||||
p.add_argument("--domain", required=True)
|
||||
p.add_argument("--nameservers", required=True)
|
||||
p.set_defaults(func=cmd_dns_set_custom)
|
||||
|
||||
p = sub.add_parser("domains.dns.getEmailForwarding", help="Get email forwarding rules")
|
||||
p.add_argument("--domain", required=True)
|
||||
p.set_defaults(func=cmd_dns_get_email_forwarding)
|
||||
|
||||
p = sub.add_parser("domains.dns.setEmailForwarding", help="Set email forwarding rules")
|
||||
p.add_argument("--domain", required=True)
|
||||
p.add_argument("--mailbox", default="")
|
||||
p.add_argument("--forward-to", default="")
|
||||
p.add_argument("--forwards", default="")
|
||||
p.set_defaults(func=cmd_dns_set_email_forwarding)
|
||||
|
||||
p = sub.add_parser("domains.ns.create", help="Create a child nameserver (glue record)")
|
||||
p.add_argument("--domain", required=True)
|
||||
p.add_argument("--nameserver", required=True)
|
||||
p.add_argument("--ip", required=True)
|
||||
p.set_defaults(func=cmd_ns_create)
|
||||
|
||||
p = sub.add_parser("domains.ns.delete", help="Delete a child nameserver")
|
||||
p.add_argument("--domain", required=True)
|
||||
p.add_argument("--nameserver", required=True)
|
||||
p.set_defaults(func=cmd_ns_delete)
|
||||
|
||||
p = sub.add_parser("domains.ns.getInfo", help="Get nameserver info")
|
||||
p.add_argument("--domain", required=True)
|
||||
p.add_argument("--nameserver", required=True)
|
||||
p.set_defaults(func=cmd_ns_get_info)
|
||||
|
||||
p = sub.add_parser("domains.ns.update", help="Update nameserver IP")
|
||||
p.add_argument("--domain", required=True)
|
||||
p.add_argument("--nameserver", required=True)
|
||||
p.add_argument("--old-ip", required=True)
|
||||
p.add_argument("--ip", required=True)
|
||||
p.set_defaults(func=cmd_ns_update)
|
||||
|
||||
p = sub.add_parser("dns.addHost", help="Add a single DNS record (preserves existing)")
|
||||
p.add_argument("--domain", required=True)
|
||||
p.add_argument("--type", required=True)
|
||||
p.add_argument("--name", required=True)
|
||||
p.add_argument("--address", required=True)
|
||||
p.add_argument("--ttl", default="1800")
|
||||
p.add_argument("--mxpref", default="")
|
||||
p.set_defaults(func=cmd_dns_add_host)
|
||||
|
||||
p = sub.add_parser("dns.removeHost", help="Remove a single DNS record")
|
||||
p.add_argument("--domain", required=True)
|
||||
p.add_argument("--type", required=True)
|
||||
p.add_argument("--name", required=True)
|
||||
p.add_argument("--address", default="")
|
||||
p.set_defaults(func=cmd_dns_remove_host)
|
||||
|
||||
return parser
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
parser = build_parser()
|
||||
args = parser.parse_args(argv)
|
||||
if not getattr(args, "command", None):
|
||||
parser.print_help()
|
||||
return 1
|
||||
try:
|
||||
args.func(args)
|
||||
except NamecheapError as exc:
|
||||
err(f"API returned error: {exc}")
|
||||
return 1
|
||||
except urllib.error.URLError as exc:
|
||||
err(f"Network error: {exc}")
|
||||
return 1
|
||||
except (OSError, ET.ParseError, json.JSONDecodeError) as exc:
|
||||
err(str(exc))
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -1,919 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Namecheap API CLI wrapper
|
||||
# Usage: ./namecheap.sh <command> [options]
|
||||
|
||||
NAMECHEAP_API_URL="https://api.namecheap.com/xml.response"
|
||||
CONFIG_FILE="$HOME/.namecheap-api"
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
CYAN='\033[0;36m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
print_error() { echo -e "${RED}Error:${NC} $1" >&2; }
|
||||
print_success() { echo -e "${GREEN}✓${NC} $1"; }
|
||||
print_info() { echo -e "${CYAN}ℹ${NC} $1"; }
|
||||
print_warn() { echo -e "${YELLOW}⚠${NC} $1"; }
|
||||
|
||||
# Load configuration
|
||||
load_config() {
|
||||
if [[ -f "$CONFIG_FILE" ]]; then
|
||||
# shellcheck source=/dev/null
|
||||
source "$CONFIG_FILE"
|
||||
fi
|
||||
}
|
||||
|
||||
# Check if credentials are configured
|
||||
check_credentials() {
|
||||
load_config
|
||||
if [[ -z "${NAMECHEAP_API_USER:-}" || -z "${NAMECHEAP_API_KEY:-}" ]]; then
|
||||
print_error "Namecheap API credentials not configured."
|
||||
echo ""
|
||||
echo "Run './namecheap.sh setup' to configure your credentials."
|
||||
echo ""
|
||||
echo "You need:"
|
||||
echo " 1. Your Namecheap username"
|
||||
echo " 2. An API key from: https://ap.www.namecheap.com/settings/tools/apiaccess/"
|
||||
echo " 3. Your public IP whitelisted in the API settings"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Get public IP address
|
||||
get_public_ip() {
|
||||
curl -s https://api.ipify.org 2>/dev/null || curl -s https://ifconfig.me 2>/dev/null || echo "unknown"
|
||||
}
|
||||
|
||||
# Make API request
|
||||
api_request() {
|
||||
local command="$1"
|
||||
shift
|
||||
local extra_params=("$@")
|
||||
|
||||
check_credentials
|
||||
local client_ip
|
||||
client_ip=$(get_public_ip)
|
||||
|
||||
local url="${NAMECHEAP_API_URL}?ApiUser=${NAMECHEAP_API_USER}&ApiKey=${NAMECHEAP_API_KEY}&UserName=${NAMECHEAP_API_USER}&Command=namecheap.${command}&ClientIp=${client_ip}"
|
||||
|
||||
for param in "${extra_params[@]}"; do
|
||||
url="${url}&${param}"
|
||||
done
|
||||
|
||||
local response
|
||||
response=$(curl -s "$url")
|
||||
|
||||
# Check for errors in the response
|
||||
if echo "$response" | grep -q 'Status="ERROR"'; then
|
||||
local error_msg
|
||||
error_msg=$(echo "$response" | grep -oP '(?<=<Err Code=")[^"]*"[^>]*>\K[^<]+' 2>/dev/null || echo "$response" | sed -n 's/.*<Err[^>]*>\(.*\)<\/Err>.*/\1/p')
|
||||
print_error "API returned error: $error_msg"
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "$response"
|
||||
}
|
||||
|
||||
# Parse domain into SLD and TLD
|
||||
parse_domain() {
|
||||
local domain="$1"
|
||||
local tld sld
|
||||
|
||||
# Handle multi-part TLDs (e.g., co.uk, com.br)
|
||||
if echo "$domain" | grep -qE '\.(co|com|net|org|gov)\.[a-z]{2}$'; then
|
||||
tld=$(echo "$domain" | grep -oE '\.[^.]+\.[^.]+$' | sed 's/^\.//')
|
||||
sld=$(echo "$domain" | sed "s/\.${tld}$//")
|
||||
else
|
||||
tld="${domain##*.}"
|
||||
sld="${domain%.*}"
|
||||
fi
|
||||
|
||||
echo "$sld" "$tld"
|
||||
}
|
||||
|
||||
# Format XML DNS records as a table
|
||||
format_dns_records() {
|
||||
local xml="$1"
|
||||
|
||||
# Extract host records
|
||||
echo ""
|
||||
printf "%-20s %-8s %-40s %-8s %-6s\n" "HOST" "TYPE" "ADDRESS" "TTL" "MXPREF"
|
||||
printf "%-20s %-8s %-40s %-8s %-6s\n" "----" "----" "-------" "---" "------"
|
||||
|
||||
echo "$xml" | grep -oP '<host[^/]*/>' | while read -r line; do
|
||||
local name type address ttl mxpref
|
||||
name=$(echo "$line" | grep -oP 'Name="\K[^"]+' || echo "")
|
||||
type=$(echo "$line" | grep -oP 'Type="\K[^"]+' || echo "")
|
||||
address=$(echo "$line" | grep -oP 'Address="\K[^"]+' || echo "")
|
||||
ttl=$(echo "$line" | grep -oP 'TTL="\K[^"]+' || echo "1800")
|
||||
mxpref=$(echo "$line" | grep -oP 'MXPref="\K[^"]+' || echo "-")
|
||||
|
||||
printf "%-20s %-8s %-40s %-8s %-6s\n" "$name" "$type" "$address" "$ttl" "$mxpref"
|
||||
done
|
||||
echo ""
|
||||
}
|
||||
|
||||
# Format domains list as a table
|
||||
format_domains_list() {
|
||||
local xml="$1"
|
||||
|
||||
echo ""
|
||||
printf "%-30s %-12s %-12s %-10s\n" "DOMAIN" "EXPIRES" "LOCKED" "AUTO-RENEW"
|
||||
printf "%-30s %-12s %-12s %-10s\n" "------" "-------" "------" "----------"
|
||||
|
||||
echo "$xml" | grep -oP '<Domain[^/]*/>' | while read -r line; do
|
||||
local name expires locked autorenew
|
||||
name=$(echo "$line" | grep -oP 'Name="\K[^"]+' || echo "")
|
||||
expires=$(echo "$line" | grep -oP 'Expires="\K[^"]+' || echo "")
|
||||
locked=$(echo "$line" | grep -oP 'IsLocked="\K[^"]+' || echo "")
|
||||
autorenew=$(echo "$line" | grep -oP 'AutoRenew="\K[^"]+' || echo "")
|
||||
|
||||
printf "%-30s %-12s %-12s %-10s\n" "$name" "$expires" "$locked" "$autorenew"
|
||||
done
|
||||
echo ""
|
||||
}
|
||||
|
||||
# Commands
|
||||
|
||||
cmd_setup() {
|
||||
echo "=== Namecheap API Setup ==="
|
||||
echo ""
|
||||
|
||||
# Show public IP
|
||||
local public_ip
|
||||
public_ip=$(get_public_ip)
|
||||
print_info "Your public IP address is: ${CYAN}${public_ip}${NC}"
|
||||
echo ""
|
||||
echo "Make sure this IP is whitelisted at:"
|
||||
echo " https://ap.www.namecheap.com/settings/tools/apiaccess/"
|
||||
echo ""
|
||||
|
||||
# Check existing config
|
||||
if [[ -f "$CONFIG_FILE" ]]; then
|
||||
load_config
|
||||
if [[ -n "${NAMECHEAP_API_USER:-}" ]]; then
|
||||
print_info "Existing configuration found for user: ${NAMECHEAP_API_USER}"
|
||||
echo ""
|
||||
|
||||
# Test the connection
|
||||
echo "Testing API connection..."
|
||||
if api_request "domains.getList" "PageSize=1" > /dev/null 2>&1; then
|
||||
print_success "API connection successful!"
|
||||
else
|
||||
print_error "API connection failed. Please check your credentials and IP whitelist."
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
# Prompt for credentials
|
||||
echo "Enter your Namecheap credentials:"
|
||||
echo ""
|
||||
read -rp " API Username: " api_user
|
||||
read -rsp " API Key: " api_key
|
||||
echo ""
|
||||
echo ""
|
||||
|
||||
if [[ -z "$api_user" || -z "$api_key" ]]; then
|
||||
print_error "Both username and API key are required."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Save configuration
|
||||
cat > "$CONFIG_FILE" << EOF
|
||||
NAMECHEAP_API_USER="${api_user}"
|
||||
NAMECHEAP_API_KEY="${api_key}"
|
||||
EOF
|
||||
chmod 600 "$CONFIG_FILE"
|
||||
print_success "Credentials saved to ${CONFIG_FILE}"
|
||||
echo ""
|
||||
|
||||
# Test connection
|
||||
load_config
|
||||
echo "Testing API connection..."
|
||||
if api_request "domains.getList" "PageSize=1" > /dev/null 2>&1; then
|
||||
print_success "API connection successful!"
|
||||
else
|
||||
print_warn "API connection failed. Please verify:"
|
||||
echo " 1. API access is enabled (ON) at the Namecheap settings page"
|
||||
echo " 2. IP address ${public_ip} is whitelisted"
|
||||
echo " 3. Your API key is correct"
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_domains_list() {
|
||||
local list_type="ALL"
|
||||
local search_term=""
|
||||
local page="1"
|
||||
local page_size="20"
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--type) list_type="$2"; shift 2 ;;
|
||||
--search) search_term="$2"; shift 2 ;;
|
||||
--page) page="$2"; shift 2 ;;
|
||||
--page-size) page_size="$2"; shift 2 ;;
|
||||
*) shift ;;
|
||||
esac
|
||||
done
|
||||
|
||||
local params=("ListType=${list_type}" "Page=${page}" "PageSize=${page_size}")
|
||||
if [[ -n "$search_term" ]]; then
|
||||
params+=("SearchTerm=${search_term}")
|
||||
fi
|
||||
|
||||
print_info "Fetching domain list..."
|
||||
local response
|
||||
response=$(api_request "domains.getList" "${params[@]}")
|
||||
format_domains_list "$response"
|
||||
}
|
||||
|
||||
cmd_dns_get_hosts() {
|
||||
local domain=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--domain) domain="$2"; shift 2 ;;
|
||||
*) shift ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$domain" ]]; then
|
||||
print_error "Domain is required. Usage: ./namecheap.sh domains.dns.getHosts --domain example.com"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
local sld tld
|
||||
read -r sld tld <<< "$(parse_domain "$domain")"
|
||||
|
||||
print_info "Fetching DNS records for ${domain} (SLD=${sld}, TLD=${tld})..."
|
||||
local response
|
||||
response=$(api_request "domains.dns.getHosts" "SLD=${sld}" "TLD=${tld}")
|
||||
format_dns_records "$response"
|
||||
}
|
||||
|
||||
cmd_dns_set_hosts() {
|
||||
local domain=""
|
||||
local hosts_file=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--domain) domain="$2"; shift 2 ;;
|
||||
--hosts) hosts_file="$2"; shift 2 ;;
|
||||
*) shift ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$domain" || -z "$hosts_file" ]]; then
|
||||
print_error "Both --domain and --hosts are required."
|
||||
echo "Usage: ./namecheap.sh domains.dns.setHosts --domain example.com --hosts hosts.json"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -f "$hosts_file" ]]; then
|
||||
print_error "Hosts file not found: ${hosts_file}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
local sld tld
|
||||
read -r sld tld <<< "$(parse_domain "$domain")"
|
||||
|
||||
# Build host parameters from JSON file
|
||||
local params=("SLD=${sld}" "TLD=${tld}")
|
||||
local i=1
|
||||
|
||||
while IFS= read -r line; do
|
||||
local hostname recordtype address ttl mxpref
|
||||
hostname=$(echo "$line" | grep -oP '"HostName"\s*:\s*"\K[^"]+' || echo "")
|
||||
recordtype=$(echo "$line" | grep -oP '"RecordType"\s*:\s*"\K[^"]+' || echo "")
|
||||
address=$(echo "$line" | grep -oP '"Address"\s*:\s*"\K[^"]+' || echo "")
|
||||
ttl=$(echo "$line" | grep -oP '"TTL"\s*:\s*"\K[^"]+' || echo "1800")
|
||||
mxpref=$(echo "$line" | grep -oP '"MXPref"\s*:\s*"\K[^"]+' || echo "")
|
||||
|
||||
if [[ -n "$hostname" && -n "$recordtype" && -n "$address" ]]; then
|
||||
params+=("HostName${i}=${hostname}")
|
||||
params+=("RecordType${i}=${recordtype}")
|
||||
params+=("Address${i}=${address}")
|
||||
params+=("TTL${i}=${ttl}")
|
||||
if [[ -n "$mxpref" ]]; then
|
||||
params+=("MXPref${i}=${mxpref}")
|
||||
fi
|
||||
((i++))
|
||||
fi
|
||||
done < <(python3 -c "
|
||||
import json, sys
|
||||
with open('${hosts_file}') as f:
|
||||
records = json.load(f)
|
||||
for r in records:
|
||||
print(json.dumps(r))
|
||||
" 2>/dev/null || jq -c '.[]' "$hosts_file")
|
||||
|
||||
if [[ $i -eq 1 ]]; then
|
||||
print_error "No valid host records found in ${hosts_file}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
print_info "Setting $((i-1)) DNS records for ${domain}..."
|
||||
local response
|
||||
response=$(api_request "domains.dns.setHosts" "${params[@]}")
|
||||
|
||||
if echo "$response" | grep -q 'IsSuccess="true"'; then
|
||||
print_success "DNS records updated successfully for ${domain}!"
|
||||
else
|
||||
print_error "Failed to update DNS records."
|
||||
echo "$response"
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_dns_add_host() {
|
||||
local domain="" record_type="" name="" address="" ttl="1800" mxpref=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--domain) domain="$2"; shift 2 ;;
|
||||
--type) record_type="$2"; shift 2 ;;
|
||||
--name) name="$2"; shift 2 ;;
|
||||
--address) address="$2"; shift 2 ;;
|
||||
--ttl) ttl="$2"; shift 2 ;;
|
||||
--mxpref) mxpref="$2"; shift 2 ;;
|
||||
*) shift ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$domain" || -z "$record_type" || -z "$name" || -z "$address" ]]; then
|
||||
print_error "Missing required parameters."
|
||||
echo "Usage: ./namecheap.sh dns.addHost --domain example.com --type A --name \"@\" --address \"1.2.3.4\" [--ttl 1800] [--mxpref 10]"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
local sld tld
|
||||
read -r sld tld <<< "$(parse_domain "$domain")"
|
||||
|
||||
# Fetch existing records
|
||||
print_info "Fetching existing DNS records for ${domain}..."
|
||||
local response
|
||||
response=$(api_request "domains.dns.getHosts" "SLD=${sld}" "TLD=${tld}")
|
||||
|
||||
# Build params with existing records + new one
|
||||
local params=("SLD=${sld}" "TLD=${tld}")
|
||||
local i=1
|
||||
|
||||
# Parse existing records
|
||||
while IFS= read -r line; do
|
||||
if [[ -z "$line" ]]; then continue; fi
|
||||
local h_name h_type h_address h_ttl h_mxpref
|
||||
h_name=$(echo "$line" | grep -oP 'Name="\K[^"]+' || echo "")
|
||||
h_type=$(echo "$line" | grep -oP 'Type="\K[^"]+' || echo "")
|
||||
h_address=$(echo "$line" | grep -oP 'Address="\K[^"]+' || echo "")
|
||||
h_ttl=$(echo "$line" | grep -oP 'TTL="\K[^"]+' || echo "1800")
|
||||
h_mxpref=$(echo "$line" | grep -oP 'MXPref="\K[^"]+' || echo "")
|
||||
|
||||
if [[ -n "$h_name" && -n "$h_type" && -n "$h_address" ]]; then
|
||||
params+=("HostName${i}=${h_name}")
|
||||
params+=("RecordType${i}=${h_type}")
|
||||
params+=("Address${i}=${h_address}")
|
||||
params+=("TTL${i}=${h_ttl}")
|
||||
if [[ -n "$h_mxpref" && "$h_mxpref" != "0" ]]; then
|
||||
params+=("MXPref${i}=${h_mxpref}")
|
||||
fi
|
||||
((i++))
|
||||
fi
|
||||
done < <(echo "$response" | grep -oP '<host[^/]*/>')
|
||||
|
||||
# Add the new record
|
||||
params+=("HostName${i}=${name}")
|
||||
params+=("RecordType${i}=${record_type}")
|
||||
params+=("Address${i}=${address}")
|
||||
params+=("TTL${i}=${ttl}")
|
||||
if [[ -n "$mxpref" ]]; then
|
||||
params+=("MXPref${i}=${mxpref}")
|
||||
fi
|
||||
|
||||
print_info "Adding ${record_type} record: ${name} -> ${address}"
|
||||
local set_response
|
||||
set_response=$(api_request "domains.dns.setHosts" "${params[@]}")
|
||||
|
||||
if echo "$set_response" | grep -q 'IsSuccess="true"'; then
|
||||
print_success "DNS record added successfully!"
|
||||
else
|
||||
print_error "Failed to add DNS record."
|
||||
echo "$set_response"
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_dns_remove_host() {
|
||||
local domain="" record_type="" name="" address=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--domain) domain="$2"; shift 2 ;;
|
||||
--type) record_type="$2"; shift 2 ;;
|
||||
--name) name="$2"; shift 2 ;;
|
||||
--address) address="$2"; shift 2 ;;
|
||||
*) shift ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$domain" || -z "$record_type" || -z "$name" ]]; then
|
||||
print_error "Missing required parameters."
|
||||
echo "Usage: ./namecheap.sh dns.removeHost --domain example.com --type A --name \"@\" [--address \"1.2.3.4\"]"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
local sld tld
|
||||
read -r sld tld <<< "$(parse_domain "$domain")"
|
||||
|
||||
# Fetch existing records
|
||||
print_info "Fetching existing DNS records for ${domain}..."
|
||||
local response
|
||||
response=$(api_request "domains.dns.getHosts" "SLD=${sld}" "TLD=${tld}")
|
||||
|
||||
# Build params excluding the record to remove
|
||||
local params=("SLD=${sld}" "TLD=${tld}")
|
||||
local i=1
|
||||
local removed=false
|
||||
|
||||
while IFS= read -r line; do
|
||||
if [[ -z "$line" ]]; then continue; fi
|
||||
local h_name h_type h_address h_ttl h_mxpref
|
||||
h_name=$(echo "$line" | grep -oP 'Name="\K[^"]+' || echo "")
|
||||
h_type=$(echo "$line" | grep -oP 'Type="\K[^"]+' || echo "")
|
||||
h_address=$(echo "$line" | grep -oP 'Address="\K[^"]+' || echo "")
|
||||
h_ttl=$(echo "$line" | grep -oP 'TTL="\K[^"]+' || echo "1800")
|
||||
h_mxpref=$(echo "$line" | grep -oP 'MXPref="\K[^"]+' || echo "")
|
||||
|
||||
# Check if this is the record to remove
|
||||
if [[ "$h_name" == "$name" && "$h_type" == "$record_type" && "$removed" == "false" ]]; then
|
||||
if [[ -z "$address" || "$h_address" == "$address" ]]; then
|
||||
removed=true
|
||||
print_info "Removing record: ${h_name} ${h_type} ${h_address}"
|
||||
continue
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -n "$h_name" && -n "$h_type" && -n "$h_address" ]]; then
|
||||
params+=("HostName${i}=${h_name}")
|
||||
params+=("RecordType${i}=${h_type}")
|
||||
params+=("Address${i}=${h_address}")
|
||||
params+=("TTL${i}=${h_ttl}")
|
||||
if [[ -n "$h_mxpref" && "$h_mxpref" != "0" ]]; then
|
||||
params+=("MXPref${i}=${h_mxpref}")
|
||||
fi
|
||||
((i++))
|
||||
fi
|
||||
done < <(echo "$response" | grep -oP '<host[^/]*/>')
|
||||
|
||||
if [[ "$removed" == "false" ]]; then
|
||||
print_error "No matching record found to remove."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# If no records left, we still need at least one (Namecheap requirement)
|
||||
if [[ $i -eq 1 ]]; then
|
||||
print_error "Cannot remove the last DNS record. Namecheap requires at least one record."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
print_info "Updating DNS records for ${domain}..."
|
||||
local set_response
|
||||
set_response=$(api_request "domains.dns.setHosts" "${params[@]}")
|
||||
|
||||
if echo "$set_response" | grep -q 'IsSuccess="true"'; then
|
||||
print_success "DNS record removed successfully!"
|
||||
else
|
||||
print_error "Failed to remove DNS record."
|
||||
echo "$set_response"
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_public_ip() {
|
||||
local ip
|
||||
ip=$(get_public_ip)
|
||||
echo "$ip"
|
||||
}
|
||||
|
||||
cmd_dns_get_list() {
|
||||
local domain=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--domain) domain="$2"; shift 2 ;;
|
||||
*) shift ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$domain" ]]; then
|
||||
print_error "Domain is required. Usage: ./namecheap.sh domains.dns.getList --domain example.com"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
local sld tld
|
||||
read -r sld tld <<< "$(parse_domain "$domain")"
|
||||
|
||||
print_info "Fetching nameservers for ${domain}..."
|
||||
local response
|
||||
response=$(api_request "domains.dns.getList" "SLD=${sld}" "TLD=${tld}")
|
||||
|
||||
local using_our_dns
|
||||
using_our_dns=$(echo "$response" | grep -oP 'IsUsingOurDNS="\K[^"]+' || echo "unknown")
|
||||
echo ""
|
||||
print_info "Using Namecheap DNS: ${using_our_dns}"
|
||||
echo ""
|
||||
echo "Nameservers:"
|
||||
echo "$response" | grep -oP '<Nameserver>\K[^<]+' | while read -r ns; do
|
||||
echo " - ${ns}"
|
||||
done
|
||||
echo ""
|
||||
}
|
||||
|
||||
cmd_dns_set_default() {
|
||||
local domain=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--domain) domain="$2"; shift 2 ;;
|
||||
*) shift ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$domain" ]]; then
|
||||
print_error "Domain is required. Usage: ./namecheap.sh domains.dns.setDefault --domain example.com"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
local sld tld
|
||||
read -r sld tld <<< "$(parse_domain "$domain")"
|
||||
|
||||
print_info "Setting ${domain} to use Namecheap default DNS..."
|
||||
local response
|
||||
response=$(api_request "domains.dns.setDefault" "SLD=${sld}" "TLD=${tld}")
|
||||
|
||||
if echo "$response" | grep -q 'Updated="true"'; then
|
||||
print_success "Domain ${domain} now uses Namecheap default DNS!"
|
||||
else
|
||||
print_error "Failed to set default DNS."
|
||||
echo "$response"
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_dns_set_custom() {
|
||||
local domain="" nameservers=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--domain) domain="$2"; shift 2 ;;
|
||||
--nameservers) nameservers="$2"; shift 2 ;;
|
||||
*) shift ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$domain" || -z "$nameservers" ]]; then
|
||||
print_error "Both --domain and --nameservers are required."
|
||||
echo "Usage: ./namecheap.sh domains.dns.setCustom --domain example.com --nameservers ns1.cloudflare.com,ns2.cloudflare.com"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
local sld tld
|
||||
read -r sld tld <<< "$(parse_domain "$domain")"
|
||||
|
||||
print_info "Setting ${domain} to use custom nameservers: ${nameservers}"
|
||||
local response
|
||||
response=$(api_request "domains.dns.setCustom" "SLD=${sld}" "TLD=${tld}" "Nameservers=${nameservers}")
|
||||
|
||||
if echo "$response" | grep -q 'Updated="true"'; then
|
||||
print_success "Domain ${domain} now uses custom nameservers!"
|
||||
else
|
||||
print_error "Failed to set custom nameservers."
|
||||
echo "$response"
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_dns_get_email_forwarding() {
|
||||
local domain=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--domain) domain="$2"; shift 2 ;;
|
||||
*) shift ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$domain" ]]; then
|
||||
print_error "Domain is required. Usage: ./namecheap.sh domains.dns.getEmailForwarding --domain example.com"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
print_info "Fetching email forwarding for ${domain}..."
|
||||
local response
|
||||
response=$(api_request "domains.dns.getEmailForwarding" "DomainName=${domain}")
|
||||
|
||||
echo ""
|
||||
printf "%-20s %-40s\n" "MAILBOX" "FORWARDS TO"
|
||||
printf "%-20s %-40s\n" "-------" "-----------"
|
||||
|
||||
echo "$response" | grep -oP '<Forward[^/]*/>' | while read -r line; do
|
||||
local mailbox forward_to
|
||||
mailbox=$(echo "$line" | grep -oP 'mailbox="\K[^"]+' || echo "")
|
||||
forward_to=$(echo "$line" | grep -oP 'ForwardTo="\K[^"]+' || echo "")
|
||||
printf "%-20s %-40s\n" "${mailbox}@${domain}" "$forward_to"
|
||||
done
|
||||
echo ""
|
||||
}
|
||||
|
||||
cmd_dns_set_email_forwarding() {
|
||||
local domain="" forwards_file=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--domain) domain="$2"; shift 2 ;;
|
||||
--forwards) forwards_file="$2"; shift 2 ;;
|
||||
--mailbox)
|
||||
# Inline single forwarding rule
|
||||
local inline_mailbox="$2"; shift 2 ;;
|
||||
--forward-to)
|
||||
local inline_forward_to="$2"; shift 2 ;;
|
||||
*) shift ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$domain" ]]; then
|
||||
print_error "Domain is required."
|
||||
echo "Usage: ./namecheap.sh domains.dns.setEmailForwarding --domain example.com --mailbox info --forward-to user@gmail.com"
|
||||
echo " or: ./namecheap.sh domains.dns.setEmailForwarding --domain example.com --forwards forwards.json"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
local params=("DomainName=${domain}")
|
||||
|
||||
if [[ -n "${inline_mailbox:-}" && -n "${inline_forward_to:-}" ]]; then
|
||||
# Single inline rule
|
||||
params+=("MailBox1=${inline_mailbox}" "ForwardTo1=${inline_forward_to}")
|
||||
elif [[ -n "$forwards_file" ]]; then
|
||||
if [[ ! -f "$forwards_file" ]]; then
|
||||
print_error "Forwards file not found: ${forwards_file}"
|
||||
exit 1
|
||||
fi
|
||||
local i=1
|
||||
while IFS= read -r line; do
|
||||
local mailbox forward_to
|
||||
mailbox=$(echo "$line" | grep -oP '"MailBox"\s*:\s*"\K[^"]+' || echo "")
|
||||
forward_to=$(echo "$line" | grep -oP '"ForwardTo"\s*:\s*"\K[^"]+' || echo "")
|
||||
if [[ -n "$mailbox" && -n "$forward_to" ]]; then
|
||||
params+=("MailBox${i}=${mailbox}" "ForwardTo${i}=${forward_to}")
|
||||
((i++))
|
||||
fi
|
||||
done < <(python3 -c "
|
||||
import json, sys
|
||||
with open('${forwards_file}') as f:
|
||||
rules = json.load(f)
|
||||
for r in rules:
|
||||
print(json.dumps(r))
|
||||
" 2>/dev/null || jq -c '.[]' "$forwards_file")
|
||||
else
|
||||
print_error "Provide either --mailbox/--forward-to or --forwards <file.json>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
print_info "Setting email forwarding for ${domain}..."
|
||||
local response
|
||||
response=$(api_request "domains.dns.setEmailForwarding" "${params[@]}")
|
||||
|
||||
if echo "$response" | grep -q 'IsSuccess="true"'; then
|
||||
print_success "Email forwarding updated for ${domain}!"
|
||||
else
|
||||
print_error "Failed to set email forwarding."
|
||||
echo "$response"
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_ns_create() {
|
||||
local domain="" nameserver="" ip=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--domain) domain="$2"; shift 2 ;;
|
||||
--nameserver) nameserver="$2"; shift 2 ;;
|
||||
--ip) ip="$2"; shift 2 ;;
|
||||
*) shift ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$domain" || -z "$nameserver" || -z "$ip" ]]; then
|
||||
print_error "Missing required parameters."
|
||||
echo "Usage: ./namecheap.sh domains.ns.create --domain example.com --nameserver ns1.example.com --ip 1.2.3.4"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
local sld tld
|
||||
read -r sld tld <<< "$(parse_domain "$domain")"
|
||||
|
||||
print_info "Creating nameserver ${nameserver} -> ${ip}..."
|
||||
local response
|
||||
response=$(api_request "domains.ns.create" "SLD=${sld}" "TLD=${tld}" "Nameserver=${nameserver}" "IP=${ip}")
|
||||
|
||||
if echo "$response" | grep -q 'IsSuccess="true"'; then
|
||||
print_success "Nameserver ${nameserver} created!"
|
||||
else
|
||||
print_error "Failed to create nameserver."
|
||||
echo "$response"
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_ns_delete() {
|
||||
local domain="" nameserver=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--domain) domain="$2"; shift 2 ;;
|
||||
--nameserver) nameserver="$2"; shift 2 ;;
|
||||
*) shift ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$domain" || -z "$nameserver" ]]; then
|
||||
print_error "Missing required parameters."
|
||||
echo "Usage: ./namecheap.sh domains.ns.delete --domain example.com --nameserver ns1.example.com"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
local sld tld
|
||||
read -r sld tld <<< "$(parse_domain "$domain")"
|
||||
|
||||
print_info "Deleting nameserver ${nameserver}..."
|
||||
local response
|
||||
response=$(api_request "domains.ns.delete" "SLD=${sld}" "TLD=${tld}" "Nameserver=${nameserver}")
|
||||
|
||||
if echo "$response" | grep -q 'IsSuccess="true"'; then
|
||||
print_success "Nameserver ${nameserver} deleted!"
|
||||
else
|
||||
print_error "Failed to delete nameserver."
|
||||
echo "$response"
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_ns_get_info() {
|
||||
local domain="" nameserver=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--domain) domain="$2"; shift 2 ;;
|
||||
--nameserver) nameserver="$2"; shift 2 ;;
|
||||
*) shift ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$domain" || -z "$nameserver" ]]; then
|
||||
print_error "Missing required parameters."
|
||||
echo "Usage: ./namecheap.sh domains.ns.getInfo --domain example.com --nameserver ns1.example.com"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
local sld tld
|
||||
read -r sld tld <<< "$(parse_domain "$domain")"
|
||||
|
||||
print_info "Fetching info for nameserver ${nameserver}..."
|
||||
local response
|
||||
response=$(api_request "domains.ns.getInfo" "SLD=${sld}" "TLD=${tld}" "Nameserver=${nameserver}")
|
||||
|
||||
local ns_ip
|
||||
ns_ip=$(echo "$response" | grep -oP 'IP="\K[^"]+' || echo "unknown")
|
||||
echo ""
|
||||
echo "Nameserver: ${nameserver}"
|
||||
echo "IP Address: ${ns_ip}"
|
||||
local statuses
|
||||
statuses=$(echo "$response" | grep -oP '<Status>\K[^<]+' | tr '\n' ', ' | sed 's/,$//')
|
||||
if [[ -n "$statuses" ]]; then
|
||||
echo "Status: ${statuses}"
|
||||
fi
|
||||
echo ""
|
||||
}
|
||||
|
||||
cmd_ns_update() {
|
||||
local domain="" nameserver="" old_ip="" new_ip=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--domain) domain="$2"; shift 2 ;;
|
||||
--nameserver) nameserver="$2"; shift 2 ;;
|
||||
--old-ip) old_ip="$2"; shift 2 ;;
|
||||
--ip) new_ip="$2"; shift 2 ;;
|
||||
*) shift ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$domain" || -z "$nameserver" || -z "$old_ip" || -z "$new_ip" ]]; then
|
||||
print_error "Missing required parameters."
|
||||
echo "Usage: ./namecheap.sh domains.ns.update --domain example.com --nameserver ns1.example.com --old-ip 1.2.3.4 --ip 5.6.7.8"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
local sld tld
|
||||
read -r sld tld <<< "$(parse_domain "$domain")"
|
||||
|
||||
print_info "Updating nameserver ${nameserver}: ${old_ip} -> ${new_ip}..."
|
||||
local response
|
||||
response=$(api_request "domains.ns.update" "SLD=${sld}" "TLD=${tld}" "Nameserver=${nameserver}" "OldIP=${old_ip}" "IP=${new_ip}")
|
||||
|
||||
if echo "$response" | grep -q 'IsSuccess="true"'; then
|
||||
print_success "Nameserver ${nameserver} updated to ${new_ip}!"
|
||||
else
|
||||
print_error "Failed to update nameserver."
|
||||
echo "$response"
|
||||
fi
|
||||
}
|
||||
|
||||
# Help
|
||||
cmd_help() {
|
||||
echo "Namecheap DNS Management CLI"
|
||||
echo ""
|
||||
echo "Usage: ./namecheap.sh <command> [options]"
|
||||
echo ""
|
||||
echo "Commands:"
|
||||
echo " setup Configure API credentials and test connection"
|
||||
echo " public-ip Show your public IP address"
|
||||
echo ""
|
||||
echo " domains.getList List your Namecheap domains"
|
||||
echo ""
|
||||
echo " domains.dns.getList Get nameservers for a domain"
|
||||
echo " domains.dns.getHosts Get DNS records for a domain"
|
||||
echo " domains.dns.setHosts Set all DNS records (from JSON file)"
|
||||
echo " domains.dns.setDefault Use Namecheap default DNS"
|
||||
echo " domains.dns.setCustom Use custom nameservers"
|
||||
echo " domains.dns.getEmailForwarding Get email forwarding rules"
|
||||
echo " domains.dns.setEmailForwarding Set email forwarding rules"
|
||||
echo ""
|
||||
echo " domains.ns.create Create a child nameserver (glue record)"
|
||||
echo " domains.ns.delete Delete a child nameserver"
|
||||
echo " domains.ns.getInfo Get nameserver info"
|
||||
echo " domains.ns.update Update nameserver IP"
|
||||
echo ""
|
||||
echo " dns.addHost Add a single DNS record (preserves existing)"
|
||||
echo " dns.removeHost Remove a single DNS record"
|
||||
echo ""
|
||||
echo "Options:"
|
||||
echo " --domain <domain> Domain name (e.g., example.com)"
|
||||
echo " --type <type> Record type (A, AAAA, CNAME, MX, TXT, etc.)"
|
||||
echo " --name <hostname> Host name (e.g., @, www, mail)"
|
||||
echo " --address <value> Record value (IP or target)"
|
||||
echo " --ttl <seconds> TTL in seconds (default: 1800)"
|
||||
echo " --mxpref <priority> MX preference (for MX records)"
|
||||
echo " --hosts <file.json> JSON file with host records"
|
||||
echo " --nameservers <ns,...> Comma-separated nameservers"
|
||||
echo " --nameserver <ns> Nameserver hostname"
|
||||
echo " --ip <address> IP address for nameserver"
|
||||
echo " --old-ip <address> Current IP (for ns.update)"
|
||||
echo " --mailbox <name> Email mailbox name"
|
||||
echo " --forward-to <email> Forward destination email"
|
||||
echo " --forwards <file.json> JSON file with forwarding rules"
|
||||
echo " --search <term> Search term for domain list"
|
||||
echo " --page <n> Page number for domain list"
|
||||
echo " --page-size <n> Page size for domain list (10-100)"
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
echo " ./namecheap.sh setup"
|
||||
echo " ./namecheap.sh domains.getList"
|
||||
echo " ./namecheap.sh domains.dns.getHosts --domain example.com"
|
||||
echo " ./namecheap.sh dns.addHost --domain example.com --type A --name www --address 1.2.3.4"
|
||||
echo " ./namecheap.sh dns.removeHost --domain example.com --type A --name www"
|
||||
echo " ./namecheap.sh domains.dns.setCustom --domain example.com --nameservers ns1.cloudflare.com,ns2.cloudflare.com"
|
||||
echo " ./namecheap.sh domains.dns.setEmailForwarding --domain example.com --mailbox info --forward-to user@gmail.com"
|
||||
echo " ./namecheap.sh domains.ns.create --domain example.com --nameserver ns1.example.com --ip 1.2.3.4"
|
||||
}
|
||||
|
||||
# Main dispatch
|
||||
main() {
|
||||
local command="${1:-help}"
|
||||
shift || true
|
||||
|
||||
case "$command" in
|
||||
setup) cmd_setup "$@" ;;
|
||||
public-ip) cmd_public_ip "$@" ;;
|
||||
domains.getList) cmd_domains_list "$@" ;;
|
||||
domains.dns.getList) cmd_dns_get_list "$@" ;;
|
||||
domains.dns.getHosts) cmd_dns_get_hosts "$@" ;;
|
||||
domains.dns.setHosts) cmd_dns_set_hosts "$@" ;;
|
||||
domains.dns.setDefault) cmd_dns_set_default "$@" ;;
|
||||
domains.dns.setCustom) cmd_dns_set_custom "$@" ;;
|
||||
domains.dns.getEmailForwarding) cmd_dns_get_email_forwarding "$@" ;;
|
||||
domains.dns.setEmailForwarding) cmd_dns_set_email_forwarding "$@" ;;
|
||||
domains.ns.create) cmd_ns_create "$@" ;;
|
||||
domains.ns.delete) cmd_ns_delete "$@" ;;
|
||||
domains.ns.getInfo) cmd_ns_get_info "$@" ;;
|
||||
domains.ns.update) cmd_ns_update "$@" ;;
|
||||
dns.addHost) cmd_dns_add_host "$@" ;;
|
||||
dns.removeHost) cmd_dns_remove_host "$@" ;;
|
||||
help|--help|-h) cmd_help ;;
|
||||
*)
|
||||
print_error "Unknown command: ${command}"
|
||||
echo ""
|
||||
cmd_help
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
main "$@"
|
||||
Reference in New Issue
Block a user