/
opt
/
dedrads
/
/opt/dedrads
mkdir
upload
Name
Size
Mode
Actions
account_review/
-
0555
rm
check_software_mods/
-
0555
rm
cms_tools/
-
0555
rm
etc/
-
0555
rm
extras/
-
0555
rm
mailparse/
-
0555
rm
mysql/
-
0555
rm
nlp_scripts/
-
0555
rm
oldrads/
-
0555
rm
perl/
-
0555
rm
provision/
-
0555
rm
python/
-
0555
rm
suspended/
-
0555
rm
__pycache__/
-
0555
rm
account-review
799
0555
edit
dl
rm
allfw
6259
0555
edit
dl
rm
alp.py
16553
0555
edit
dl
rm
autossl_sync
6267
0555
edit
dl
rm
check_apache
9205
0555
edit
dl
rm
check_bandwidth
4546
0555
edit
dl
rm
check_boxtrapper
1684
0555
edit
dl
rm
check_cpu
12212
0555
edit
dl
rm
check_crons
1827
0555
edit
dl
rm
check_dcpumon
1973
0555
edit
dl
rm
check_dns
1827
0555
edit
dl
rm
check_domlogs
61
0555
edit
dl
rm
check_exim
3731
0555
edit
dl
rm
check_hacks
22604
0555
edit
dl
rm
check_imap
5249
0555
edit
dl
rm
check_io
6635
0555
edit
dl
rm
check_max_children
16341
0555
edit
dl
rm
check_mem
1660
0555
edit
dl
rm
check_misc
1510
0555
edit
dl
rm
check_pacct
5201
0555
edit
dl
rm
check_php
4344
0555
edit
dl
rm
check_pop3
2295
0555
edit
dl
rm
check_prov.py
4714
0555
edit
dl
rm
check_server
696
0555
edit
dl
rm
check_size
490
0555
edit
dl
rm
check_software
9849
0555
edit
dl
rm
check_spamd
2426
0555
edit
dl
rm
check_traffic
1933
0555
edit
dl
rm
check_user
1887
0555
edit
dl
rm
clean_exim.py
6390
0555
edit
dl
rm
clusterfix
5343
0555
edit
dl
rm
cmspass.py
3970
0555
edit
dl
rm
cms_check.py
9825
0555
edit
dl
rm
cms_counter.py
36734
0555
edit
dl
rm
cms_creds
713
0555
edit
dl
rm
cms_dumpdb
4485
0555
edit
dl
rm
cms_pw
1416
0555
edit
dl
rm
cpanel-api
7040
0555
edit
dl
rm
cpanel_postgres_manager.py
29004
0555
edit
dl
rm
cpmerge
4631
0555
edit
dl
rm
cpumon
2195
0555
edit
dl
rm
dcpumon.pl
1239
0555
edit
dl
rm
dedcheck
18127
0555
edit
dl
rm
dns-sync
14386
0555
edit
dl
rm
docroot.py
4425
0555
edit
dl
rm
du-tree
7450
0555
edit
dl
rm
envinfo.py
6633
0555
edit
dl
rm
exclude_rbl.py
5249
0555
edit
dl
rm
extract-vhost
2691
0555
edit
dl
rm
filescan
3566
0555
edit
dl
rm
find_warez
1460
0555
edit
dl
rm
first_setup.py
19120
0555
edit
dl
rm
fixwpcron.py
4822
0555
edit
dl
rm
fix_dns_cluster
353
0555
edit
dl
rm
goaccess
1217720
0555
edit
dl
rm
hostsfilemods
4467
0555
edit
dl
rm
imap_io
2323
0555
edit
dl
rm
innodb_converter.py
9343
0555
edit
dl
rm
killall911
530
0555
edit
dl
rm
killdns
941
0555
edit
dl
rm
lastcommcache.sh
1374
0555
edit
dl
rm
legal_lock_down.sh
7450
0555
edit
dl
rm
lil-cpanel
52002
0555
edit
dl
rm
listacct
4575
0555
edit
dl
rm
mailscan
4544
0555
edit
dl
rm
mail_sources.py
7407
0555
edit
dl
rm
megaclisas-status
38664
0555
edit
dl
rm
migrate2central.sh
14418
0555
edit
dl
rm
modify-account
40631
0555
edit
dl
rm
modsec_disable.py
11461
0555
edit
dl
rm
monarxctl
8912
0555
edit
dl
rm
msp.pl
27714
0555
edit
dl
rm
mysql_dstat
474
0555
edit
dl
rm
mysql_selector.py
73102
0555
edit
dl
rm
nlp
4582
0555
edit
dl
rm
procscrape
1863
0555
edit
dl
rm
quarantine
11083
0555
edit
dl
rm
radsfunctions.sh
863
0555
edit
dl
rm
recent-cp
16148
0555
edit
dl
rm
remote_dump
18347
0555
edit
dl
rm
reset_cpanel
3205
0555
edit
dl
rm
server-load
2192
0555
edit
dl
rm
show-conns
20105
0555
edit
dl
rm
show-conns-adv.py
54667
0555
edit
dl
rm
sparta.py
382498
0555
edit
dl
rm
sqltop
27129
0555
edit
dl
rm
telcheck
2411
0555
edit
dl
rm
temprootreset
186
0555
edit
dl
rm
unsuspend_user
1307
0555
edit
dl
rm
unsusprunner.sh
1840
0555
edit
dl
rm
updatednsadmin
526
0555
edit
dl
rm
update_spf
14972
0555
edit
dl
rm
upgrade-check
9216
0555
edit
dl
rm
vhost_data.py
6221
0555
edit
dl
rm
wp-xray.sh
85816
0555
edit
dl
rm
Edit:
/opt/dedrads/update_spf
(14972B)
#!/usr/lib/rads/venv/bin/python3 """SPF record updater""" # Vanessa Vasile, InMotion Hosting, Oct 2017 # ref: https://trac.imhtech.net/T3/ticket/18096 from argparse import ArgumentParser, Action, ArgumentTypeError as BadArg import platform import sys from typing import Literal from subprocess import CalledProcessError, check_output from collections import OrderedDict from cpapis import whmapi1, cpapi2, CpAPIError from rads import cpuser_safe from rads.color import red, yellow, green, magenta, blue HOSTNAME = platform.node() SPAMEXP_HOSTNAME = 'smtp.servconfig.com' def get_args() -> ( tuple[Literal['create', 'update', 'delete'], bool, str, list[str]] ): """Arg Parser""" parser = ArgumentParser(description='Configures SPF records') actions_group = parser.add_mutually_exclusive_group(required=True) targets_group = parser.add_mutually_exclusive_group(required=True) # fmt: off actions_group.add_argument( '--create', '-c', action='store_const', const='create', dest='action', help='Creates a new SPF record and applies it. ' 'Will overwrite an existing one.', ) actions_group.add_argument( '--update', '-r', action='store_const', const='update', dest='action', help='Updates the existing SPF record to be correct', ) parser.add_argument( '--show-only', '-s', action='store_true', help='Do not apply the new record, just show it', ) actions_group.add_argument( '--delete', '-x', action='store_const', const='delete', dest='action', help='Deletes an SPF TXT record', ) targets_group.add_argument( '--user', '-u', action=SetUserAction, help='Update all domains (main, addon, parked) for this user', ) targets_group.add_argument( '--domain', '-d', action=SetDomainAction, dest='domains', help='Domain to update', ) # fmt: on args = parser.parse_args() return args.action, args.show_only, args.user, args.domains class SetUserAction(Action): def __call__(self, parser, namespace, values, option_string=None): user: str = values if not cpuser_safe(user): raise BadArg(f"{user} is not a valid cPanel user or is restricted") try: api = cpapi2('DomainLookup::getbasedomains', user) domains = [x['domain'] for x in api['cpanelresult']['data']] except Exception as exc: print(type(exc).__name__, exc, file=sys.stderr) raise BadArg("Error getting domain list from cpapi2") from exc setattr(namespace, 'domains', domains) setattr(namespace, 'user', user) class SetDomainAction(Action): def __call__(self, parser, namespace, values, option_string=None): domain: str = values user = whoowns(domain) if not user: raise BadArg(f"{domain} is not owned by a user on the system") if not cpuser_safe(user): raise BadArg(f"{user} is a restricted user") setattr(namespace, 'domains', [domain]) def err_msg(msg: str) -> None: print(red(msg), file=sys.stderr) def main(): action, show_only, user, domains = get_args() spf_data: dict[str, tuple[int, str]] = {} for domain in domains: spf_data.update(get_spf(domain)) if user is None and not spf_data and action != 'create': # if user is None, --domain / -d was used err_msg( f"{domains[0]} does not have an SPF record. " "Use --create/-c instead" ) sys.exit(1) mail_ips, domain_mail_ips = get_mail_ips(list(spf_data.keys())) if action == 'update': for domain, (line_num, txtdata) in spf_data.items(): new_record = generate_spf( existing_spf=txtdata, mail_ips=mail_ips, domain=domain, domain_ip=domain_mail_ips.get(domain, None), overwrite=False, ) print(blue(f"{domain}:")) print(magenta(f'\tExisting: "{txtdata}"')) if show_only: print(yellow(f'\tNew: "{new_record}"')) continue print(green(f'\tNew: "{new_record}"')) update_zone(domain, domain, line_num, new_record) elif action == 'create': for domain in domains: line_num, txtdata = spf_data.get(domain, (None, None)) new_record = generate_spf( existing_spf=None, mail_ips=mail_ips, domain=domain, domain_ip=domain_mail_ips.get(domain, None), overwrite=True, ) print(blue(f"{domain}:")) print(magenta(f'\tExisting: "{txtdata}"')) if show_only: print(yellow(f'\tNew: "{new_record}"')) continue print(green(f'\tNew: "{new_record}"')) if txtdata is None: # add new record update_zone(domain, domain, None, new_record) else: # update existing update_zone(domain, domain, line_num, new_record) elif action == 'delete': for domain, (line_num, txtdata) in spf_data.items(): print(blue(f"{domain}:")) print(magenta(f'\tExisting: "{txtdata}"')) if show_only: continue delete_spf(domain, txtdata) else: raise RuntimeError(f"{action=}") def get_mail_ips(domains: list[str]) -> tuple[list[str], dict[str, str]]: """Pulls relevant data""" mail_ips = [] for path in ('/var/cpanel/mainip', '/var/cpanel/mainips/root'): try: with open(path, encoding='utf-8') as file: for line in file: mail_ips.append(line.strip()) except OSError: pass domain_mail_ips = {} try: with open('/etc/mailips', encoding='utf-8') as file: for line in file: dom, ipaddr = line.strip().split(':') if dom in domains: domain_mail_ips[dom] = ipaddr.strip() except ValueError: err_msg('/etc/mailips is incorrectly formatted') sys.exit(1) except OSError: pass return mail_ips, domain_mail_ips def whoowns(domain: str) -> str: """Obtain the cPanel user owning a domain""" try: user = check_output( ['/usr/local/cpanel/scripts/whoowns', domain], encoding='utf-8' ) except CalledProcessError: return '' return user.strip() def get_spf(domain: str) -> dict[str, tuple[int, str]]: """Gets the SPF record for a domain""" try: api = whmapi1('dumpzone', {"domain": domain}, check=True) except CpAPIError as exc: err_msg(str(exc)) return {} spf_records = {} for data in api['data']['zone'][0]['record']: data: dict record_type: str = data.get('type', '') txtdata: str = data.get('txtdata', '') if 'TXT' not in record_type or 'v=spf1' not in txtdata: continue line: int = data['Line'] name: str = data['name'].rstrip('.') # If we have a duplicate key, error out. This is invalid. if name in spf_records: err_msg( f"Duplicate SPF record for {name} Resolve this manually, " "then re-run the utility" ) sys.exit(1) spf_records[name] = (line, txtdata) return spf_records def update_zone(domain: str, name: str, line: int, content: str) -> None: """Updates the DNS zone""" if not name.endswith('.'): name = name + '.' args = { 'domain': domain, 'name': name, 'class': 'IN', 'ttl': 900, 'type': 'TXT', 'txtdata': content, } try: if line is not None: # We're updating an existing record whmapi1('editzonerecord', args | {'line': line}, check=True) else: # We're creating a new zone record whmapi1('addzonerecord', args, check=True) except CpAPIError as exc: print(red(f'\tError updating zone: {exc}')) def delete_spf(domain: str, line: str) -> None: """Deletes an SPF record""" try: whmapi1('removezonerecord', {'zone': domain, 'line': line}, check=True) except CpAPIError as exc: print(red(f'\tError updating zone: {exc}')) else: print(green('\tSPF TXT record removed')) def generate_spf( *, mail_ips: list[str], existing_spf: str, domain: str, domain_ip: str | None, overwrite: bool, ): """Generates the SPF record""" # The only difference between 'creating' and 'updating' a record is # 'updating' will ignore stuff that's already in the record, # unless it's wrong if overwrite or existing_spf is None: # If we're overwriting the record, we don't really care what's in it # This is the default record to include SE and server hostname # (all mail ips on this server) if domain_ip: new_record = 'v=spf1 +a +mx +a:{} +a:{} +ip4:{} ~all'.format( HOSTNAME, SPAMEXP_HOSTNAME, domain_ip, ) else: new_record = 'v=spf1 +a +mx +a:{} +a:{} ~all'.format( HOSTNAME, SPAMEXP_HOSTNAME, ) return new_record # If we have a record already, split it up so it's easier to parse parts = existing_spf.split() # Now we have to further split each part into sections # See: http://www.openspf.org/SPF_Record_Syntax mechanisms = ['+', '-', '~', '?'] data = OrderedDict() index = 0 for part in parts: if part.startswith('v='): data['version'] = {'mech': '', 'qual': '', 'val': part} index += 1 continue # Check for modifiers if part.startswith('redirect='): _, val = part.split('=') if val != '': # If we have a modifier, we can basically just generate a # new record new_record = 'v=spf1 redirect=%s' % (val) return new_record # If we're missing a value though, remove this entirely continue if part.startswith('exp='): _, val = part.split('=') if val != '': data['exp'] = {'mech': '', 'qual': '', 'val': 'exp=%s' % val} continue # If the part starts with a mechanism... if any(part.startswith(m) for m in mechanisms): mech = part[0] part = part.lstrip(part[0]) else: mech = '' # If the part contains a ":", add a value. Otherwise replace with a '' if ':' in part: qual, val = part.split(':') else: qual = part val = '' data[index] = {'mech': mech, 'qual': qual, 'val': val} index += 1 # This is where we actually generate the record present = { 'version': False, 'server_host': False, 'domain_mx': False, 'domain_a': False, 'domain_mailip': False, 'se_host': False, 'pass_fail': False, } delete = [] for key, value in data.items(): # ipv4 that matches mailips or domain ip if value['qual'] == 'ip4': if value['val'] in mail_ips: delete.append(key) if domain_ip is None: # we don't need this value if the domain uses a shared IP present['domain_mailip'] = True else: if value['val'] == domain_ip: present['domain_mailip'] = True # Other servers' hostnames of ours if value['val'] not in (HOSTNAME, SPAMEXP_HOSTNAME): if ( 'inmotionhosting.com' in value['val'] or 'webhostinghub.com' in value['val'] or 'servconfig.com' in value['val'] ): delete.append(key) # version if value['qual'] == '' and "v=" in value['val']: present['version'] = True # domain, server, & SE hostnames if value['qual'] == 'a': if value['val'] == HOSTNAME: present['server_host'] = True if value['val'] == SPAMEXP_HOSTNAME: present['se_host'] = True if value['val'] == domain or value['val'] == '': present['domain_a'] = True # The domain's MX if value['qual'] == 'mx': if value['val'] == domain or value['val'] == '': present['domain_mx'] = True # action if value['qual'] == "all": present['pass_fail'] = True data['pass'] = value if isinstance(key, int): # delete the numerical key delete.append(key) for item in delete: data.pop(item) # Add anything that's missing if not present['domain_mailip'] and domain_ip is not None: data[index] = {'mech': '+', 'qual': 'a', 'val': domain_ip} index += 1 if not present['version']: data['version'] = {'mech': '', 'qual': '', 'val': 'v=spf1'} index += 1 if not present['server_host']: data[index] = {'mech': '+', 'qual': 'a', 'val': HOSTNAME} index += 1 if not present['se_host']: data[index] = {'mech': '+', 'qual': 'a', 'val': SPAMEXP_HOSTNAME} index += 1 if not present['domain_a']: data[index] = {'mech': '+', 'qual': 'a', 'val': ''} index += 1 if not present['domain_mx']: data[index] = {'mech': '+', 'qual': 'mx', 'val': ''} index += 1 if not present['pass_fail']: data['pass'] = {'mech': '~', 'qual': 'all', 'val': ''} index += 1 # Now put all the parts together to make a new SPF record new_map = [] new_map.append(data['version']) # version first for key, val in sorted(data.items()): if isinstance(key, int): new_map.append(val) # The pass/fail and exp modifier should be last try: new_map.append(data['pass']) except KeyError: pass try: new_map.append(data['exp']) except KeyError: pass # Now put all the SPF elements together data = [] # reuse this one for item in new_map: if item['mech'] != '': if item['qual'] != '': line = '{}{}'.format(item['mech'], item['qual']) else: line = item['mech'] else: line = '%s' % item['qual'] if item['val'] != '' and item['qual'] != '': line = '{}:{}'.format(line, item['val']) else: line = '{}{}'.format(line, item['val']) data.append(line.strip()) new_record = ' '.join(data) return new_record if __name__ == "__main__": main()
Save
cmd:
run