- README НИИКН: обновлён Mailcow (пароль, DNS, порты, ящик) - changelog: добавлена запись о настройке mail.niikn.com - credentials: добавлены Spaceweb, Mailcow НИИКН, NPM НИИКН - decisions: сценарий настройки почтового сервера (шаблон для dttb.ru) - snippets: скрипт spaceweb-dns-api.py для управления DNS Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
168 lines
5.7 KiB
Python
168 lines
5.7 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
Spaceweb DNS API — управление DNS записями через JSON-RPC.
|
||
Панель: https://vps.sweb.ru
|
||
Домены: niikn.com, dttb.ru, itilegent.ru
|
||
|
||
Использование:
|
||
python3 spaceweb-dns-api.py info niikn.com
|
||
python3 spaceweb-dns-api.py add-mx niikn.com mail.niikn.com 10
|
||
python3 spaceweb-dns-api.py add-txt niikn.com @ "v=spf1 mx ~all"
|
||
python3 spaceweb-dns-api.py add-txt niikn.com _dmarc "v=DMARC1; p=none"
|
||
python3 spaceweb-dns-api.py del niikn.com TXT 1
|
||
python3 spaceweb-dns-api.py zone niikn.com
|
||
"""
|
||
|
||
import urllib.request
|
||
import urllib.parse
|
||
import json
|
||
import http.cookiejar
|
||
import sys
|
||
|
||
LOGIN = "it5870yand"
|
||
PASSWORD = "1qaz!QAZ"
|
||
UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
|
||
|
||
|
||
def create_session():
|
||
"""Логин в Spaceweb, возвращает opener с куками."""
|
||
cj = http.cookiejar.CookieJar()
|
||
opener = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(cj))
|
||
|
||
# GET login page
|
||
req = urllib.request.Request("https://mcp.sweb.ru/")
|
||
req.add_header("User-Agent", UA)
|
||
opener.open(req)
|
||
|
||
# POST auth
|
||
login_data = urllib.parse.urlencode({
|
||
"login": LOGIN, "password": PASSWORD,
|
||
"new_panel": "1", "to": "//mcp.sweb.ru/main/index/", "savepref": ""
|
||
}).encode()
|
||
req = urllib.request.Request("https://mcp.sweb.ru/main/auth_submit/",
|
||
data=login_data, method="POST")
|
||
req.add_header("Content-Type", "application/x-www-form-urlencoded")
|
||
req.add_header("User-Agent", UA)
|
||
req.add_header("Referer", "https://mcp.sweb.ru/main/auth/")
|
||
resp = opener.open(req)
|
||
if "vps.sweb.ru" not in resp.url and "mcp.sweb.ru/main/index" not in resp.url:
|
||
raise RuntimeError(f"Login failed, redirected to: {resp.url}")
|
||
return opener
|
||
|
||
|
||
def api_call(opener, method, params):
|
||
"""Вызов JSON-RPC API Spaceweb."""
|
||
payload = {
|
||
"jsonrpc": "2.0",
|
||
"id": 1,
|
||
"user": LOGIN,
|
||
"method": method,
|
||
"params": params
|
||
}
|
||
body = urllib.parse.quote(json.dumps(payload, separators=(",", ":")), safe="").encode()
|
||
req = urllib.request.Request("https://api.sweb.ru/domains/dns",
|
||
data=body, method="POST")
|
||
req.add_header("Content-Type", "application/x-www-form-urlencoded")
|
||
req.add_header("User-Agent", UA)
|
||
req.add_header("Origin", "https://vps.sweb.ru")
|
||
req.add_header("Referer", "https://vps.sweb.ru/")
|
||
resp = opener.open(req)
|
||
result = json.loads(resp.read().decode())
|
||
if "error" in result:
|
||
raise RuntimeError(f"API error: {result['error']['message']}")
|
||
return result.get("result")
|
||
|
||
|
||
def cmd_info(opener, domain):
|
||
"""Показать все DNS записи."""
|
||
records = api_call(opener, "info", {"domain": domain})
|
||
print(f"DNS записи для {domain}:")
|
||
for r in records:
|
||
t = r.get("type", "?")
|
||
n = r.get("name", "@") or "@"
|
||
v = r.get("value", "")
|
||
idx = r.get("index", "?")
|
||
extra = f" (pri: {r.get('priority')})" if r.get("priority") else ""
|
||
print(f" [{idx:>2}] {t:5s} {n:30s} → {v[:100]}{extra}")
|
||
|
||
|
||
def cmd_zone(opener, domain):
|
||
"""Показать zone file."""
|
||
result = api_call(opener, "getFile", {"domain": domain})
|
||
print(result["content"])
|
||
|
||
|
||
def cmd_add_mx(opener, domain, value, priority="10"):
|
||
"""Добавить MX запись."""
|
||
if not value.endswith("."):
|
||
value += "."
|
||
result = api_call(opener, "editMx", {
|
||
"domain": domain, "subDomain": "", "action": "add",
|
||
"priority": str(priority), "value": value
|
||
})
|
||
print(f"MX добавлен: {domain} → {value} (pri {priority}): {result}")
|
||
|
||
|
||
def cmd_add_txt(opener, domain, subdomain, value):
|
||
"""Добавить TXT запись."""
|
||
result = api_call(opener, "editTxt", {
|
||
"domain": domain, "action": "add",
|
||
"subDomain": subdomain, "value": value
|
||
})
|
||
print(f"TXT добавлен: {subdomain}.{domain} → {value[:60]}...: {result}")
|
||
|
||
|
||
def cmd_add_a(opener, domain, name, ip):
|
||
"""Добавить A запись (субдомен)."""
|
||
result = api_call(opener, "editMain", {
|
||
"domain": domain, "action": "add",
|
||
"name": name, "type": "A", "value": ip, "prefix": ""
|
||
})
|
||
print(f"A добавлен: {name}.{domain} → {ip}: {result}")
|
||
|
||
|
||
def cmd_del(opener, domain, record_type, index):
|
||
"""Удалить запись по типу и индексу."""
|
||
method_map = {
|
||
"A": "editMain", "AAAA": "editMain", "CNAME": "editMain",
|
||
"MX": "editMx", "TXT": "editTxt", "NS": "editNs",
|
||
"SRV": "editSrv", "CAA": "editCaa"
|
||
}
|
||
method = method_map.get(record_type.upper(), "editMain")
|
||
result = api_call(opener, method, {
|
||
"domain": domain, "action": "del",
|
||
"index": int(index), "type": record_type.upper()
|
||
})
|
||
print(f"Удалено: {record_type} index={index}: {result}")
|
||
|
||
|
||
def main():
|
||
if len(sys.argv) < 3:
|
||
print(__doc__)
|
||
sys.exit(1)
|
||
|
||
cmd = sys.argv[1]
|
||
domain = sys.argv[2]
|
||
opener = create_session()
|
||
|
||
if cmd == "info":
|
||
cmd_info(opener, domain)
|
||
elif cmd == "zone":
|
||
cmd_zone(opener, domain)
|
||
elif cmd == "add-mx" and len(sys.argv) >= 4:
|
||
pri = sys.argv[4] if len(sys.argv) > 4 else "10"
|
||
cmd_add_mx(opener, domain, sys.argv[3], pri)
|
||
elif cmd == "add-txt" and len(sys.argv) >= 5:
|
||
cmd_add_txt(opener, domain, sys.argv[3], sys.argv[4])
|
||
elif cmd == "add-a" and len(sys.argv) >= 5:
|
||
cmd_add_a(opener, domain, sys.argv[3], sys.argv[4])
|
||
elif cmd == "del" and len(sys.argv) >= 5:
|
||
cmd_del(opener, domain, sys.argv[3], sys.argv[4])
|
||
else:
|
||
print(__doc__)
|
||
sys.exit(1)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|