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): 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*([^;\}]+);', block, re.S | re.I) if m: return [ip.strip() for ip in m.group(1).split(';') if ip.strip()] return []