dns_manager.py 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155
  1. import os
  2. import re
  3. import time
  4. import datetime
  5. import subprocess
  6. import config
  7. from core.file_manager import read_file, write_file
  8. def backup_zone(domain, content):
  9. ts = int(time.time())
  10. write_file(os.path.join(config.BACKUP_DIR, f"{domain}_{ts}.db"), content)
  11. return ts
  12. def get_backups(domain):
  13. if not os.path.exists(config.BACKUP_DIR): return []
  14. files = [f for f in os.listdir(config.BACKUP_DIR) if f.startswith(f"{domain}_") and f.endswith(".db")]
  15. backups = []
  16. for f in sorted(files, reverse=True):
  17. ts = f.replace(f"{domain}_", "").replace(".db", "")
  18. dt = datetime.datetime.fromtimestamp(int(ts)).strftime("%Y-%m-%d %H:%M:%S")
  19. backups.append({"file": f, "time": dt, "ts": ts})
  20. return backups
  21. def check_zone_syntax(domain, content):
  22. tmp_path = os.path.join("/tmp", f"bind_check_{domain}_{int(time.time())}.db")
  23. write_file(tmp_path, content)
  24. res = subprocess.run(["named-checkzone", domain, tmp_path], capture_output=True, text=True)
  25. os.remove(tmp_path)
  26. if res.returncode == 0: return True, ""
  27. else: return False, res.stdout.replace(tmp_path, 'zone_file').strip()
  28. def get_ds_records(domain):
  29. try:
  30. res = subprocess.run(["rndc", "dnssec", "-status", domain], capture_output=True, text=True)
  31. if res.returncode == 0: return [line for line in res.stdout.split('\n') if line]
  32. except: pass
  33. return []
  34. def apply_template(content, domain, t_data):
  35. replacements = {
  36. "{DOMAIN}": domain, "{SERIAL}": time.strftime('%Y%m%d01'),
  37. "{TTL}": str(t_data.get('ttl', '86400')), "{REFRESH}": str(t_data.get('refresh', '3600')),
  38. "{RETRY}": str(t_data.get('retry', '1800')), "{EXPIRE}": str(t_data.get('expire', '604800'))
  39. }
  40. for k, v in t_data.get('custom_vars', {}).items(): replacements[f"{{{k.upper()}}}"] = str(v)
  41. loops = 0
  42. while loops < 10:
  43. old_content = content
  44. for k, v in replacements.items(): content = content.replace(k, v)
  45. if old_content == content: break
  46. loops += 1
  47. return content
  48. def sanitize_record_name(name, domain):
  49. name = name.strip()
  50. if name.endswith('.'): return name
  51. if domain.endswith('.in-addr.arpa'):
  52. base_ip = ".".join(reversed(domain.replace('.in-addr.arpa', '').split('.')))
  53. if name.startswith(base_ip + "."):
  54. remainder = name[len(base_ip)+1:]
  55. rem_reversed = ".".join(reversed(remainder.split('.')))
  56. return f"{rem_reversed}.{domain}."
  57. elif re.match(r'^\d+(?:\.\d+)*$', name):
  58. rem_reversed = ".".join(reversed(name.split('.')))
  59. return f"{rem_reversed}.{domain}."
  60. if name != '@' and not name.endswith('.'): return f"{name}.{domain}."
  61. return name
  62. def get_zone_block(conf, domain):
  63. parts = conf.split('zone "')
  64. for p in parts[1:]:
  65. if p.startswith(domain + '"'): return 'zone "' + p
  66. return None
  67. def get_zone_file_path(domain):
  68. conf = read_file(config.LOCAL_CONF)
  69. block = get_zone_block(conf, domain)
  70. if block:
  71. m = re.search(r'file\s+"([^"]+)";', block)
  72. if m:
  73. p = m.group(1)
  74. return p if p.startswith('/') else os.path.join(config.CACHE_PATH if "type slave" in block else config.BIND_PATH, p)
  75. if domain == ".": return os.path.join(config.BIND_PATH, "db.root")
  76. return os.path.join(config.BIND_PATH, f"db.{domain}")
  77. def parse_zone_file(path, domain=None):
  78. records = {"A":[], "AAAA":[], "CNAME":[], "MX":[], "TXT":[], "NS":[], "SRV":[], "PTR":[]}
  79. content = read_file(path)
  80. if domain and domain != "." and ('\0' in content or 'SOA' not in content.upper()):
  81. try:
  82. res = subprocess.run(["named-compilezone", "-q", "-f", "raw", "-F", "text", "-o", "-", domain, path], capture_output=True, text=True)
  83. if res.returncode == 0: content = res.stdout
  84. except: pass
  85. if not content: return records
  86. is_reverse = domain and domain.endswith('.in-addr.arpa')
  87. for line in content.split('\n'):
  88. clean = line.split(';')[0].strip()
  89. if not clean or (domain != "." and 'SOA' in clean) or clean.startswith('$'): continue
  90. tokens = clean.split()
  91. ridx = -1
  92. for i, t in enumerate(tokens):
  93. if t.upper() in records: ridx = i; break
  94. if ridx != -1:
  95. rtype = tokens[ridx].upper()
  96. name = re.sub(r'\.{2,}', '.', tokens[0])
  97. if name.upper() in ['IN', '@'] or name.isdigit(): name = "@"
  98. val = clean.split(tokens[ridx], 1)[1].strip() if rtype == 'TXT' else " ".join(tokens[ridx+1:])
  99. friendly_name = name
  100. if is_reverse and name != '@':
  101. clean_name = name.lower().replace('.in-addr.arpa.', '')
  102. parts = clean_name.split('.')
  103. if all(p.isdigit() for p in parts): friendly_name = ".".join(reversed(parts))
  104. records[rtype].append({"name": name, "friendly_name": friendly_name, "value": val, "raw": line})
  105. return records
  106. def increment_serial(content):
  107. match = re.search(r'(\d+)\s*;\s*Serial', content)
  108. if match:
  109. old_serial = match.group(1)
  110. new_serial = str(int(old_serial) + 1)
  111. return content.replace(old_serial, new_serial)
  112. return content
  113. def construct_record_value(req_form, rtype):
  114. v = req_form.get('value', '').strip()
  115. if not v: return ""
  116. if rtype == 'TXT':
  117. v = v.strip('"')
  118. return f'"{v}"'
  119. if rtype in ['PTR', 'CNAME', 'NS'] and not v.endswith('.'): v = f"{v}."
  120. elif rtype in ['MX', 'SRV']:
  121. parts = v.split()
  122. if len(parts) >= 2 and not parts[-1].endswith('.'):
  123. parts[-1] = f"{parts[-1]}."
  124. v = " ".join(parts)
  125. return v
  126. def get_zone_masters_list(domain):
  127. """
  128. Επιστρέφει μια λίστα με IPs των Masters.
  129. Αν είναι Master ζώνη, επιστρέφει κενή λίστα (θα τη χειριστεί ο cluster_manager).
  130. """
  131. conf = read_file(config.LOCAL_CONF)
  132. block = get_zone_block(conf, domain)
  133. if not block: return []
  134. if "type slave" in block or "type forward" in block:
  135. # Ψάχνουμε IPs μέσα στα άγκιστρα { ... }
  136. m = re.search(r'(?:masters|forwarders)\s*\{\s*([^;\}]+);', block, re.S | re.I)
  137. if m:
  138. return [ip.strip() for ip in m.group(1).split(';') if ip.strip()]
  139. return []