/opt/dedrads
NameSizeModeActions
account_review/-0555rm
check_software_mods/-0555rm
cms_tools/-0555rm
etc/-0555rm
extras/-0555rm
mailparse/-0555rm
mysql/-0555rm
nlp_scripts/-0555rm
oldrads/-0555rm
perl/-0555rm
provision/-0555rm
python/-0555rm
suspended/-0555rm
__pycache__/-0555rm
account-review7990555editdlrm
allfw62590555editdlrm
alp.py165530555editdlrm
autossl_sync62670555editdlrm
check_apache92050555editdlrm
check_bandwidth45460555editdlrm
check_boxtrapper16840555editdlrm
check_cpu122120555editdlrm
check_crons18270555editdlrm
check_dcpumon19730555editdlrm
check_dns18270555editdlrm
check_domlogs610555editdlrm
check_exim37310555editdlrm
check_hacks226040555editdlrm
check_imap52490555editdlrm
check_io66350555editdlrm
check_max_children163410555editdlrm
check_mem16600555editdlrm
check_misc15100555editdlrm
check_pacct52010555editdlrm
check_php43440555editdlrm
check_pop322950555editdlrm
check_prov.py47140555editdlrm
check_server6960555editdlrm
check_size4900555editdlrm
check_software98490555editdlrm
check_spamd24260555editdlrm
check_traffic19330555editdlrm
check_user18870555editdlrm
clean_exim.py63900555editdlrm
clusterfix53430555editdlrm
cmspass.py39700555editdlrm
cms_check.py98250555editdlrm
cms_counter.py367340555editdlrm
cms_creds7130555editdlrm
cms_dumpdb44850555editdlrm
cms_pw14160555editdlrm
cpanel-api70400555editdlrm
cpanel_postgres_manager.py290040555editdlrm
cpmerge46310555editdlrm
cpumon21950555editdlrm
dcpumon.pl12390555editdlrm
dedcheck181270555editdlrm
dns-sync143860555editdlrm
docroot.py44250555editdlrm
du-tree74500555editdlrm
envinfo.py66330555editdlrm
exclude_rbl.py52490555editdlrm
extract-vhost26910555editdlrm
filescan35660555editdlrm
find_warez14600555editdlrm
first_setup.py191200555editdlrm
fixwpcron.py48220555editdlrm
fix_dns_cluster3530555editdlrm
goaccess12177200555editdlrm
hostsfilemods44670555editdlrm
imap_io23230555editdlrm
innodb_converter.py93430555editdlrm
killall9115300555editdlrm
killdns9410555editdlrm
lastcommcache.sh13740555editdlrm
legal_lock_down.sh74500555editdlrm
lil-cpanel520020555editdlrm
listacct45750555editdlrm
mailscan45440555editdlrm
mail_sources.py74070555editdlrm
megaclisas-status386640555editdlrm
migrate2central.sh144180555editdlrm
modify-account406310555editdlrm
modsec_disable.py114610555editdlrm
monarxctl89120555editdlrm
msp.pl277140555editdlrm
mysql_dstat4740555editdlrm
mysql_selector.py731020555editdlrm
nlp45820555editdlrm
procscrape18630555editdlrm
quarantine110830555editdlrm
radsfunctions.sh8630555editdlrm
recent-cp161480555editdlrm
remote_dump183470555editdlrm
reset_cpanel32050555editdlrm
server-load21920555editdlrm
show-conns201050555editdlrm
show-conns-adv.py546670555editdlrm
sparta.py3824980555editdlrm
sqltop271290555editdlrm
telcheck24110555editdlrm
temprootreset1860555editdlrm
unsuspend_user13070555editdlrm
unsusprunner.sh18400555editdlrm
updatednsadmin5260555editdlrm
update_spf149720555editdlrm
upgrade-check92160555editdlrm
vhost_data.py62210555editdlrm
wp-xray.sh858160555editdlrm
Edit: /opt/dedrads/dns-sync (14386B)
#!/usr/lib/rads/venv/bin/python3 """ Attempt to sync a DNS zone to our cluster, and print any known errors. Provides a bunch of additional checking for common causes of issues syncing. Wraps dnscluster synczone and reads /usr/local/cpanel/logs/error_log for a result. -- Common errors -- Error 401: Unauthorized This means there is something wrong with the DNS cluster key located in /var/cpanel/cluster/. This could mean that the DNS key isn't active, or that it needs to be installed from PowerPanel. For VPS+ accounts, this can mean that the domain's cPanel account has a reseller that is not set up with the user's current PowerPanel API key. In rare cases, the key may need to be activated directly on the DNS authority server by Systems. Error 409: Conflict DNS authority for this domain is owned by another server. If the other server is verified, reset DNS authority for the domain. Always follow policy for this. No result when searching for log entry in /usr/local/cpanel/logs/error_log This typically indicates that dnsadmin didn't attempt to sync the zone to our servers. DNS cluster key not set up On VPS+ this indicates there is no DNS cluster configuration file for the reseller account that owns the cPanel account the domain belongs to. If activating the DNS key through PowerPanel does not resolve this, check if the reseller account is the primary account in PowerPanel. If it is not, copy the DNS cluster configuration from /var/cpanel/cluster/ for the primary reseller into a new directory with the name of the the correct reseller. e.g. rsync -avP /var/cpanel/cluster/mainreseller5/ /var/cpanel/cluster/reseller2 On shared, this simply means the DNS key needs to be activated from PowerPanel. Reseller does not exist on the server This means the domain's cPanel account has a reseller owner that isn't on the server. Typically the result of a server transfer, simply assign a valid reseller owner to the domain's cPanel account. """ import re import subprocess import sys from argparse import ArgumentParser, RawTextHelpFormatter from datetime import datetime from os import isatty from os.path import exists from cpapis import whmapi1 from rads import whoowns, cpuser_safe class SyncError(Exception): """ An exception raised when an issue is detected when attempting to sync DNS to the cluster, either preventing it from succeeding or a detected failure. """ class SerialUpdateError(Exception): """ An exception raised when an issue is detected when attempting to update the serial number in an SOA record, either preventing it from succeeding or a detected failure """ def get_args(): parser = ArgumentParser( description=__doc__, formatter_class=RawTextHelpFormatter ) # fmt: off parser.add_argument( "domains", metavar="domain", nargs='+', default=[], help="Domain(s) to sync", ) parser.add_argument( "-s", "--serial-update", default=False, action="store_true", help="Force SOA serial update", ) # fmt: on return parser.parse_args() def process_entry(domain, error, error_code, message): """ Process status message from a dnsadmin log line """ reply = re.search(r" server replied: (.+)", message) user_msg = "" if error: user_msg += f"{error}. " success = False if reply: if "Success" in reply.group(1): user_msg += "Successfully synced to DNS cluster" success = True else: user_msg += reply.group(1) else: user_msg += message if error_code: status_messages = { "409": ( "Zone ownership may need to be reset in the DNS authority " "server." ), "401": ( "This may mean that the DNS key needs to be activated, " "reset, or installed. Check /var/cpanel/cluster/" ), "500": ( "Inspect cPanel error log and logs on DNS" " authority server" ), } if error_code in status_messages: user_msg += f". {status_messages[error_code]}" if success: log_success(domain, user_msg) else: log_error(domain, user_msg) def sync_zone(domain): """ Attempt to sync a zone to the cluster """ owner = whoowns(domain) if not owner: raise SyncError( "Failed to locate cPanel owner of domain. Is it on this server?" ) if not cpuser_safe(owner): raise SyncError( f"Unable to synchronize domain owned by secure user {owner}" ) if not exists(f"/var/cpanel/cluster/{owner}/config/imh"): dns_owner = reseller_owner(owner) if not exists(f"/var/cpanel/cluster/{dns_owner}/config/imh"): if dns_owner in ["inmotion", "hubhost"]: # Shared does per-user raise SyncError( f"DNS cluster key is not set up for {owner} or " f"reseller {dns_owner}. On shared, this only needs to be " "configured for the user" ) raise SyncError( "DNS cluster key not set up for " f"reseller {dns_owner}. Unable to sync." ) try: synczone_args = [ "/usr/local/cpanel/scripts/dnscluster", "synczone", domain, ] with subprocess.Popen( synczone_args, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL ) as proc: output = proc.communicate() except FileNotFoundError: sys.exit( "Unable to locate /usr/local/cpanel/scripts/dnscluster on server. " "Is this a cPanel server?" ) sync_output = output[0].decode("utf-8", "ignore") if "...Done" not in sync_output: # Print as an error, trim "Done" line trimmed = sync_output.replace("\nDone\n", "") raise SyncError(trimmed) return True def sync_zones(domains): """Sync domains to cluster, reading the cPanel error log for results""" with open("/usr/local/cpanel/logs/error_log", encoding='utf-8') as log: for domain in [domain.lower() for domain in domains]: log.seek(0, 2) # Seek to the end, because we are tailing the file try: sync_zone(domain) except SyncError as e: log_error(domain, str(e)) continue entry_found = False # Search for log entry like: # [2020-10-19 17:50:59 +0000] info [dnsadmin] update_zone (domain) for entry in reversed(log.readlines()): entry = entry.strip() year = r"\d{4}" month = day = hour = minute = second = r"\d{2}" # We're doing it this way to make this regex slightly more # readable. date = f"{year}-{month}-{day} {hour}:{minute}:{second}" # We are very strict about the format as quite a lot is dumped # into this log file. I apologize to future me (or whomever) # that have to maintain this. match = re.search( ( r"^\[{} [+-]\d+\] \w* \[dnsadmin\] " r"update_zone \({}\) (Error (\d+): \w+)?(.*)$" ).format(date, domain), entry, ) if match: entry_found = True try: process_entry( domain, match.group(1), match.group(2), match.group(3), ) except SyncError as e: log_error(domain, str(e)) if not entry_found: log_error( domain, ( "No result when searching for log entry in " "/usr/local/cpanel/logs/error_log" ), ) def fetch_zone(domain): """ Fetch zone file from cPanel for a given domain. Raises SerialUpdateError upon failure """ response = whmapi1("dumpzone", {"domain": domain}) if not response: raise SerialUpdateError( "An unknown error occured running whmapi1 dumpzone. " "This could be due to a missing access hash or network " "error" ) if "metadata" in response: status_code = response["metadata"].get("result") reason = response["metadata"].get("reason") if status_code != 1: raise SerialUpdateError( "Unable to update serial. whmapi1 dumpzone " f"failed. {reason}" ) if "data" not in response: # If we hit this block, I have no idea what's wrong. We're gonna # panic and print the full error message. print(response, file=sys.stderr) raise SerialUpdateError( "Unable to update serial. whmapi1 dumpzone returned no data." ) data = response["data"] if ( "zone" not in data or len(data["zone"]) == 0 or "record" not in data["zone"][0] ): raise SerialUpdateError( "Unable to update serial. whmapi1 dumpzone returned no zone." ) return data["zone"][0]["record"] def update_serial(domain): """ Update zone file serial in SOA record for a given domain Raised SerialUpdateError on failure """ owner = whoowns(domain) if owner and not cpuser_safe(owner): raise SerialUpdateError( f"Unable to update SOA for domain owned by secure user {owner}" ) zone_file_path = f"/var/named/{domain}.db" if not exists(zone_file_path): raise SerialUpdateError( "Unable to update serial, zone file doesn't exist." ) zone = fetch_zone(domain) if not zone: raise SerialUpdateError("Unable to update serial. Zone file is empty") soa_record = None for record in zone: if record.get("name") == f"{domain}." and record.get("type") == "SOA": soa_record = record break if not soa_record: raise SerialUpdateError("SOA record not found in zone file") serial = str(soa_record["serial"]) date_fmt = "%Y%m%d" # e.g Oct 1 is 20201001 new_date = int(datetime.today().strftime(date_fmt)) counter = 0 if len(serial) != 10: # e.g. 2020101000 serial_date = new_date # Not a standard format, overwrite. else: try: serial_date = int(serial[:-2]) counter = int(serial[-2:]) + 1 except ValueError: serial_date = new_date if serial_date < new_date: # Preserve 'future' date serial_date = new_date counter = 0 zone_file_path = f"/var/named/{domain}.db" with open(zone_file_path, encoding='utf-8') as zone_file: lines = zone_file.readlines() new_serial = f"{serial_date}{str(counter).zfill(2)}" line = soa_record["Line"] record_len = soa_record.get("Lines", 1) # There's at least one line soa_lines = lines[line : line + record_len] for i in range(record_len): lines[line + i] = re.sub( fr"^([^;]+){serial}", fr"\g<1>{new_serial}", soa_lines[i] ) with open(zone_file_path, "w", encoding='utf-8') as zone_file: zone_file.write("".join(lines)) # Finally, write changes def update_serials(domains): """ Update zone file serial in SOA record for given domains """ for domain in [domain.lower() for domain in domains]: try: update_serial(domain) except SerialUpdateError as err: log_error(domain, str(err)) def reseller_owner(owner): """ Get a user's reseller owner, including root. """ try: with open("/etc/trueuserowners", encoding="utf-8") as file: match = next(x for x in file if x.startswith(f"{owner}: ")) # userna5: root return match.rstrip().split(": ")[1] except FileNotFoundError as e: raise SyncError( f"Unable to find {e.filename}. " "Is this a cPanel server?" ) from e except OSError as e: raise SyncError( f"Unable to load {e.filename}. " "Do you have sufficient permissions?" ) from e except StopIteration as exc: raise SyncError( f"Reseller {owner} does not exist on the server" ) from exc def log_error(domain, message): print(f"{style(domain, 'red')}: {message}") def log_success(domain, message): print(f"{style(domain, 'green')}: {message}") def style(text, color=None): """ Stylize text with ANSI escape codes for red/green, if we're in a terminal """ if not isatty(sys.stdin.fileno()) or color is None: return text # Only output color to a terminal colors = {"red": 31, "green": 32} if color and color not in colors: raise ValueError(f"Color {color} isn't accepted") return "".join([f"\033[{colors[color]}m{text}\033[0m"]) def main(): parsed = get_args() if exists("/etc/ansible/wordpress-ultrastack"): sys.exit( style( "DNS is not able to be managed by this container. Please manage " "your DNS on Platform i.\n" "https://www.inmotionhosting.com/support/product-guides/wordpress-hosting/central/domains/dns-management/", 'red', ) ) if not exists("/var/cpanel/useclusteringdns"): sys.exit( "DNS clustering isn't enabled on this server, please enable it!" ) for domain in parsed.domains: if ".." in domain: # I can't think of a valid path traversal exploit using this tool, # however, I can't think of a reason to risk it either sys.exit(f"{domain} is not a valid domain name") if parsed.serial_update: update_serials(parsed.domains) sync_zones(parsed.domains) if __name__ == "__main__": main()