This commit is contained in:
Daniel Langbein 2023-03-27 21:29:52 +02:00
commit bc17a5d458
4 changed files with 116 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
/.idea/
/cfg/
/venv/

24
README.md Normal file
View File

@ -0,0 +1,24 @@
# netcup DNS
Update DNS records with your current external IP address using the netcup DNS API.
## TODOs
Alternative external IP detection:
```python
def external_ip_upnp():
"""
https://stackoverflow.com/a/41385033
Didn't work for me. Even after double checking fritz.box settings:
fritz.box > Heimnetz > Netzwerk > Statusinformationen über UPnP übertragen
"""
import miniupnpc
u = miniupnpc.UPnP()
u.discoverdelay = 1000
u.discover()
u.selectigd()
print('external ip address: {}'.format(u.externalipaddress()))
```

88
main.py Normal file
View File

@ -0,0 +1,88 @@
import configparser
import ipaddress
from pathlib import Path
import requests
from nc_dnsapi import Client, DNSRecord
def main():
"""
https://github.com/nbuchwitz/nc_dnsapi
"""
destination = external_ipv4()
files = [file for file in Path('cfg').iterdir() if file.name.endswith('.cfg') and file.is_file()]
for file in files:
cfg = configparser.ConfigParser()
cfg.read(file)
customer = cfg['credentials']['customer']
api_key = cfg['credentials']['api_key']
api_password = cfg['credentials']['api_password']
domains = [section for section in cfg.sections() if section != 'credentials']
with Client(customer, api_key, api_password) as api:
for domain in domains:
hostname = cfg[domain]['hostname']
type_ = cfg[domain]['type']
update_record_destination(api, domain, hostname, type_, destination)
def update_record_destination(api: Client, domain: str, hostname: str, type_: str, destination: str) -> None:
record = get_record(api, domain, hostname, type_)
record.destination = destination
api.update_dns_record(domain, record)
def get_record(api: Client, domain: str, hostname: str, type_: str) -> DNSRecord:
records: list[DNSRecord] = api.dns_records(domain)
record: DNSRecord
matches = [record for record in records if record.hostname == hostname and record.type == type_]
if len(matches) != 1:
raise Exception(f'Expected one DNSRecord for {hostname}.{domain}, but got {len(matches)}')
return matches[0]
def external_ipv4() -> str:
"""
:return: Public IPv4 address
"""
# IPv4 only.
endpoints = ['https://checkipv4.dedyn.io/', 'https://api.ipify.org', 'https://v4.ident.me/']
# Not sure if they return IPv4 addresses only,
# so we try these endpoints last.
endpoints += ['https://ipinfo.io/ip']
for endpoint in endpoints:
backup = None
try:
# Force the usage of IPv4
# https://stackoverflow.com/a/72440253/6334421
#
# Alternatively, use urllib3: https://stackoverflow.com/a/46972341/6334421
backup = requests.packages.urllib3.util.connection.HAS_IPV6
requests.packages.urllib3.util.connection.HAS_IPV6 = False
# Timeout after 5 seconds.
ip = requests.get(endpoint, timeout=5).text.strip()
# Check if it is actually an IPv4 address.
# Some services, such as e.g. v4.ident.me, sometimes return IPv6.
ipv4 = ipaddress.ip_address(ip)
if not isinstance(ipv4, ipaddress.IPv4Address):
continue
# Return ip address as string.
return ipv4.exploded
except Exception as _e:
continue
finally:
# Allow usage of IPv6 again.
if backup is not None:
requests.packages.urllib3.util.connection.HAS_IPV6 = backup
raise Exception('Could not determine public IPv4 address.')
if __name__ == '__main__':
main()

1
requirements.txt Normal file
View File

@ -0,0 +1 @@
nc-dnsapi