| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155 |
- import os
- import re
- import time
- import datetime
- import subprocess
- import config
- from core.file_manager import read_file, write_file
- def backup_zone(domain, content):
- ts = int(time.time())
- write_file(os.path.join(config.BACKUP_DIR, f"{domain}_{ts}.db"), content)
- return ts
- def get_backups(domain):
- if not os.path.exists(config.BACKUP_DIR): return []
- files = [f for f in os.listdir(config.BACKUP_DIR) if f.startswith(f"{domain}_") and f.endswith(".db")]
- backups = []
- for f in sorted(files, reverse=True):
- ts = f.replace(f"{domain}_", "").replace(".db", "")
- dt = datetime.datetime.fromtimestamp(int(ts)).strftime("%Y-%m-%d %H:%M:%S")
- backups.append({"file": f, "time": dt, "ts": ts})
- return backups
- def check_zone_syntax(domain, content):
- tmp_path = os.path.join("/tmp", f"bind_check_{domain}_{int(time.time())}.db")
- write_file(tmp_path, content)
- res = subprocess.run(["named-checkzone", domain, tmp_path], capture_output=True, text=True)
- os.remove(tmp_path)
- if res.returncode == 0: return True, ""
- else: return False, res.stdout.replace(tmp_path, 'zone_file').strip()
- def get_ds_records(domain):
- try:
- res = subprocess.run(["rndc", "dnssec", "-status", domain], capture_output=True, text=True)
- if res.returncode == 0: return [line for line in res.stdout.split('\n') if line]
- except: pass
- return []
- def apply_template(content, domain, t_data):
- replacements = {
- "{DOMAIN}": domain, "{SERIAL}": time.strftime('%Y%m%d01'),
- "{TTL}": str(t_data.get('ttl', '86400')), "{REFRESH}": str(t_data.get('refresh', '3600')),
- "{RETRY}": str(t_data.get('retry', '1800')), "{EXPIRE}": str(t_data.get('expire', '604800'))
- }
- for k, v in t_data.get('custom_vars', {}).items(): replacements[f"{{{k.upper()}}}"] = str(v)
- loops = 0
- while loops < 10:
- old_content = content
- for k, v in replacements.items(): content = content.replace(k, v)
- if old_content == content: break
- loops += 1
- return content
- def sanitize_record_name(name, domain):
- name = name.strip()
- if name.endswith('.'): return name
- if domain.endswith('.in-addr.arpa'):
- base_ip = ".".join(reversed(domain.replace('.in-addr.arpa', '').split('.')))
- if name.startswith(base_ip + "."):
- remainder = name[len(base_ip)+1:]
- rem_reversed = ".".join(reversed(remainder.split('.')))
- return f"{rem_reversed}.{domain}."
- elif re.match(r'^\d+(?:\.\d+)*$', name):
- rem_reversed = ".".join(reversed(name.split('.')))
- return f"{rem_reversed}.{domain}."
- if name != '@' and not name.endswith('.'): return f"{name}.{domain}."
- return name
- def get_zone_block(conf, domain):
- parts = conf.split('zone "')
- for p in parts[1:]:
- if p.startswith(domain + '"'): return 'zone "' + p
- return None
- def get_zone_file_path(domain):
- conf = read_file(config.LOCAL_CONF)
- block = get_zone_block(conf, domain)
- if block:
- m = re.search(r'file\s+"([^"]+)";', block)
- if m:
- p = m.group(1)
- return p if p.startswith('/') else os.path.join(config.CACHE_PATH if "type slave" in block else config.BIND_PATH, p)
- if domain == ".": return os.path.join(config.BIND_PATH, "db.root")
- return os.path.join(config.BIND_PATH, f"db.{domain}")
- def parse_zone_file(path, domain=None):
- records = {"A":[], "AAAA":[], "CNAME":[], "MX":[], "TXT":[], "NS":[], "SRV":[], "PTR":[]}
- content = read_file(path)
- if domain and domain != "." and ('\0' in content or 'SOA' not in content.upper()):
- try:
- res = subprocess.run(["named-compilezone", "-q", "-f", "raw", "-F", "text", "-o", "-", domain, path], capture_output=True, text=True)
- if res.returncode == 0: content = res.stdout
- except: pass
- if not content: return records
-
- is_reverse = domain and domain.endswith('.in-addr.arpa')
- for line in content.split('\n'):
- clean = line.split(';')[0].strip()
- if not clean or (domain != "." and 'SOA' in clean) or clean.startswith('$'): continue
- tokens = clean.split()
- ridx = -1
- for i, t in enumerate(tokens):
- if t.upper() in records: ridx = i; break
- if ridx != -1:
- rtype = tokens[ridx].upper()
- name = re.sub(r'\.{2,}', '.', tokens[0])
- if name.upper() in ['IN', '@'] or name.isdigit(): name = "@"
- val = clean.split(tokens[ridx], 1)[1].strip() if rtype == 'TXT' else " ".join(tokens[ridx+1:])
-
- friendly_name = name
- if is_reverse and name != '@':
- clean_name = name.lower().replace('.in-addr.arpa.', '')
- parts = clean_name.split('.')
- if all(p.isdigit() for p in parts): friendly_name = ".".join(reversed(parts))
-
- records[rtype].append({"name": name, "friendly_name": friendly_name, "value": val, "raw": line})
- return records
- def increment_serial(content):
- match = re.search(r'(\d+)\s*;\s*Serial', content)
- if match:
- old_serial = match.group(1)
- new_serial = str(int(old_serial) + 1)
- return content.replace(old_serial, new_serial)
- return content
- def construct_record_value(req_form, rtype):
- v = req_form.get('value', '').strip()
- if not v: return ""
- if rtype == 'TXT':
- v = v.strip('"')
- return f'"{v}"'
- if rtype in ['PTR', 'CNAME', 'NS'] and not v.endswith('.'): v = f"{v}."
- elif rtype in ['MX', 'SRV']:
- parts = v.split()
- if len(parts) >= 2 and not parts[-1].endswith('.'):
- parts[-1] = f"{parts[-1]}."
- v = " ".join(parts)
- return v
-
- def get_zone_masters_list(domain):
- """
- Επιστρέφει μια λίστα με IPs των Masters.
- Αν είναι Master ζώνη, επιστρέφει κενή λίστα (θα τη χειριστεί ο cluster_manager).
- """
- conf = read_file(config.LOCAL_CONF)
- block = get_zone_block(conf, domain)
- if not block: return []
-
- if "type slave" in block or "type forward" in block:
- # Ψάχνουμε IPs μέσα στα άγκιστρα { ... }
- m = re.search(r'(?:masters|forwarders)\s*\{\s*([^;\}]+);', block, re.S | re.I)
- if m:
- return [ip.strip() for ip in m.group(1).split(';') if ip.strip()]
- return []
|