dns_manager.py 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165
  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 + '"'):
  66. block = 'zone "' + p
  67. open_braces = 0
  68. in_block = False
  69. # Μετράμε αγκύλια για να βρούμε το τέλος της ζώνης με ακρίβεια
  70. for i, char in enumerate(block):
  71. if char == '{':
  72. open_braces += 1
  73. in_block = True
  74. elif char == '}':
  75. open_braces -= 1
  76. if in_block and open_braces == 0:
  77. if i + 1 < len(block) and block[i+1] == ';': return block[:i+2]
  78. return block[:i+1]
  79. return block
  80. return None
  81. def get_zone_file_path(domain):
  82. conf = read_file(config.LOCAL_CONF)
  83. block = get_zone_block(conf, domain)
  84. if block:
  85. m = re.search(r'file\s+"([^"]+)";', block)
  86. if m:
  87. p = m.group(1)
  88. return p if p.startswith('/') else os.path.join(config.CACHE_PATH if "type slave" in block else config.BIND_PATH, p)
  89. if domain == ".": return os.path.join(config.BIND_PATH, "db.root")
  90. return os.path.join(config.BIND_PATH, f"db.{domain}")
  91. def parse_zone_file(path, domain=None):
  92. records = {"A":[], "AAAA":[], "CNAME":[], "MX":[], "TXT":[], "NS":[], "SRV":[], "PTR":[]}
  93. content = read_file(path)
  94. if domain and domain != "." and ('\0' in content or 'SOA' not in content.upper()):
  95. try:
  96. res = subprocess.run(["named-compilezone", "-q", "-f", "raw", "-F", "text", "-o", "-", domain, path], capture_output=True, text=True)
  97. if res.returncode == 0: content = res.stdout
  98. except: pass
  99. if not content: return records
  100. is_reverse = domain and domain.endswith('.in-addr.arpa')
  101. for line in content.split('\n'):
  102. clean = line.split(';')[0].strip()
  103. if not clean or (domain != "." and 'SOA' in clean) or clean.startswith('$'): continue
  104. tokens = clean.split()
  105. ridx = -1
  106. for i, t in enumerate(tokens):
  107. if t.upper() in records: ridx = i; break
  108. if ridx != -1:
  109. rtype = tokens[ridx].upper()
  110. name = re.sub(r'\.{2,}', '.', tokens[0])
  111. if name.upper() in ['IN', '@'] or name.isdigit(): name = "@"
  112. val = clean.split(tokens[ridx], 1)[1].strip() if rtype == 'TXT' else " ".join(tokens[ridx+1:])
  113. friendly_name = name
  114. if is_reverse and name != '@':
  115. clean_name = name.lower().replace('.in-addr.arpa.', '')
  116. parts = clean_name.split('.')
  117. if all(p.isdigit() for p in parts): friendly_name = ".".join(reversed(parts))
  118. records[rtype].append({"name": name, "friendly_name": friendly_name, "value": val, "raw": line})
  119. return records
  120. def increment_serial(content):
  121. match = re.search(r'(\d+)\s*;\s*Serial', content)
  122. if match:
  123. old_serial = match.group(1)
  124. new_serial = str(int(old_serial) + 1)
  125. return content.replace(old_serial, new_serial)
  126. return content
  127. def construct_record_value(req_form, rtype):
  128. v = req_form.get('value', '').strip()
  129. if not v: return ""
  130. if rtype == 'TXT':
  131. v = v.strip('"')
  132. return f'"{v}"'
  133. if rtype in ['PTR', 'CNAME', 'NS'] and not v.endswith('.'): v = f"{v}."
  134. elif rtype in ['MX', 'SRV']:
  135. parts = v.split()
  136. if len(parts) >= 2 and not parts[-1].endswith('.'):
  137. parts[-1] = f"{parts[-1]}."
  138. v = " ".join(parts)
  139. return v
  140. def get_zone_masters_list(domain):
  141. conf = read_file(config.LOCAL_CONF)
  142. block = get_zone_block(conf, domain)
  143. if not block: return []
  144. if "type slave" in block or "type forward" in block:
  145. import re
  146. m = re.search(r'(?:masters|forwarders)\s*\{\s*(.*?)\s*\};', block, re.S | re.I)
  147. if m:
  148. ips_raw = m.group(1).replace('\n', ' ')
  149. return [ip.strip() for ip in ips_raw.split(';') if ip.strip()]
  150. return []