import os import re import time import datetime import subprocess import secrets 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): # FIX 3: Μοναδικό temp file για να μην υπάρξει ποτέ σύγκρουση (Race Condition) tmp_path = os.path.join("/tmp", f"bind_check_{domain}_{secrets.token_hex(4)}.db") write_file(tmp_path, content) res = subprocess.run(["named-checkzone", domain, tmp_path], capture_output=True, text=True) try: os.remove(tmp_path) except: pass 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 + '"'): block = 'zone "' + p open_braces = 0 in_block = False for i, char in enumerate(block): if char == '{': open_braces += 1 in_block = True elif char == '}': open_braces -= 1 if in_block and open_braces == 0: if i + 1 < len(block) and block[i+1] == ';': return block[:i+2] return block[:i+1] return block 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 = "@" # FIX 2: Αλεξίσφαιρη ανάγνωση της τιμής χωρίς να σπάει αν το name περιέχει "TXT" ή "A" match = re.search(rf'\s+(?:IN\s+)?{rtype}\s+(.*)', clean, re.IGNORECASE) val = match.group(1).strip() if match 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): # FIX 1 (CRITICAL): Έξυπνος SOA Serial Parser # Αντί να ψάχνει σχόλια, βρίσκει το "SOA", ανοίγει την παρένθεση "(" και αρπάζει τον 1ο αριθμό που θα βρει! match = re.search(r'(SOA\s+.*?\(\s*)(\d+)', content, re.IGNORECASE | re.DOTALL) if match: prefix = match.group(1) old_serial = match.group(2) today_prefix = time.strftime('%Y%m%d') # Αν το serial είναι ήδη σημερινό (π.χ. 2026042001), του προσθέτουμε +1 if old_serial.startswith(today_prefix): new_serial = str(int(old_serial) + 1) else: # Αλλιώς ξεκινάμε με σημερινή ημερομηνία + 01 new_serial = today_prefix + "01" return content[:match.start()] + prefix + new_serial + content[match.end():] 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): 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: import re m = re.search(r'(?:masters|forwarders)\s*\{\s*(.*?)\s*\};', block, re.S | re.I) if m: ips_raw = m.group(1).replace('\n', ' ') return [ip.strip() for ip in ips_raw.split(';') if ip.strip()] return []