Skip to content

Commit ea0f584

Browse files
committed
Added modules for new functionality
1 parent 8b23312 commit ea0f584

File tree

4 files changed

+178
-0
lines changed

4 files changed

+178
-0
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
__pycache__

modules/dns_utils.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# modules/dns_utils.py
2+
import dns.resolver
3+
import dns.exception
4+
import logging
5+
6+
logger = logging.getLogger("one_com_ddns")
7+
8+
def get_authoritative_ns(domain):
9+
"""
10+
Recursively finds the authoritative nameservers for a given domain.
11+
Tries parent domains if no NS records are found or in case of timeout or other DNS errors
12+
(except for NXDOMAIN, where it stops and indicates domain does not exist).
13+
14+
Args:
15+
domain (str): The domain to query.
16+
17+
Returns:
18+
list: A list of authoritative nameserver hostnames, or None if not found or domain does not exist.
19+
"""
20+
try:
21+
answers = dns.resolver.resolve(domain, 'NS')
22+
return [data.to_text() for data in answers]
23+
except dns.resolver.NoAnswer:
24+
exception_type = "NoAnswer"
25+
except dns.exception.Timeout:
26+
exception_type = "Timeout"
27+
except dns.resolver.NXDOMAIN:
28+
exception_type = "NXDOMAIN"
29+
except dns.exception.DNSException as e:
30+
exception_type = f"DNSException: {e}"
31+
32+
# Handle exceptions that might indicate trying parent domain
33+
if exception_type in ["NoAnswer", "Timeout", "DNSException", "NXDOMAIN"]:
34+
parts = domain.split('.')
35+
if len(parts) > 2: # Check if there's a parent domain
36+
parent_domain = '.'.join(parts[1:])
37+
return get_authoritative_ns(parent_domain)
38+
else:
39+
logger.warning(f"No authoritative NS for {domain} and no parent domain to try after exception: {exception_type}")
40+
return None # No parent domain, give up
41+
else: # Should not reach here, but as a fallback. For completeness.
42+
logger.error(f"Failed to get NS for {domain} due to: {exception_type}")
43+
return None
44+
45+
def get_ip_and_ttl(domain, ns_servers=None):
46+
"""
47+
Gets the IP address and TTL of a domain from the authoritative nameservers.
48+
49+
Args:
50+
domain (str): The domain to query.
51+
optional ns_servers (list): A list of authoritative nameserver hostnames.
52+
53+
Returns:
54+
tuple: A tuple containing the IP address and TTL, or None if not found.
55+
"""
56+
if ns_servers is None:
57+
ns_servers = get_authoritative_ns(domain)
58+
if ns_servers is None:
59+
return None
60+
resolver = dns.resolver.Resolver()
61+
tmp = [dns.resolver.resolve(ns, 'A')[0].address for ns in ns_servers]
62+
resolver.nameservers = tmp
63+
try:
64+
answers = resolver.resolve(domain, 'A')
65+
for rdata in answers:
66+
return rdata.address, answers.ttl
67+
except (dns.resolver.NoAnswer, dns.resolver.NXDOMAIN, dns.exception.Timeout, dns.exception.DNSException) as e:
68+
logger.warning(f"Error getting IP and TTL for {domain}: {e}")
69+
return None

modules/logger.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import logging
2+
3+
GRAY = '\033[90m'
4+
RED = '\033[91m'
5+
YELLOW = '\033[93m'
6+
RESET = '\033[0m'
7+
8+
class ColoredFormatter(logging.Formatter):
9+
"""A formatter that adds colors based on log level."""
10+
11+
def format(self, record):
12+
log_level = record.levelname
13+
if log_level == "DEBUG":
14+
log_level_colored = f"{GRAY}[DEBUG]{RESET}"
15+
elif log_level == "WARNING":
16+
log_level_colored = f"{YELLOW}[WARN]{RESET}"
17+
elif log_level == "ERROR":
18+
log_level_colored = f"{RED}[ERROR]{RESET}"
19+
else:
20+
log_level_colored = f"[{log_level}]" # No color for INFO
21+
22+
return logging.Formatter(f'{log_level_colored} %(message)s').format(record)
23+
24+
def setup_logging(level=logging.INFO):
25+
"""Sets up centralized logging for the application."""
26+
formatter = ColoredFormatter()
27+
handler = logging.StreamHandler()
28+
handler.setFormatter(formatter)
29+
30+
logger = logging.getLogger("one_com_ddns")
31+
logger.setLevel(level)
32+
logger.addHandler(handler)
33+
34+
return logger

modules/one_com_config.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
# modules/one_com_config.py
2+
import os
3+
import argparse
4+
import requests
5+
import logging
6+
7+
logger = logging.getLogger("one_com_ddns")
8+
9+
def parse_config(validate_required=True):
10+
"""
11+
Parses configuration for the one.com DDNS script from command-line arguments,
12+
environment variables (ONECOM_*), and returns None as default if no value is set.
13+
14+
Configuration is prioritized: command-line arguments > environment variables > None (if unset).
15+
16+
Key configuration parameters:
17+
- username (-u, --username): one.com username (defaults to None if unset)
18+
- password (-p, --password): one.com password (defaults to None if unset)
19+
- domains (-d, --domains): List of domain names (e.g.,-d example.com example2.com) (defaults to None if unset)
20+
- ip (-i, --ip): IP address source ('AUTO', 'ARG', or IP) (defaults to None if unset)
21+
- force-update (-f, --force-update): Force DNS update (defaults to None if unset)
22+
- ttl (-t, --ttl): TTL for DNS records (defaults to None if unset)
23+
- skip-confirmation (--skip-confirmation): Skip confirmation prompts (defaults to False if unset)
24+
25+
Returns:
26+
argparse.Namespace: Object containing configuration parameters.
27+
28+
Raises:
29+
ValueError: If validate_required is True and username, password, or domain are None
30+
after parsing.
31+
SystemExit: If automatic IP retrieval fails when 'AUTO' is selected
32+
or if no IP address is provided as a command-line argument
33+
when 'ARG' is selected.
34+
"""
35+
parser = argparse.ArgumentParser(description="one.com DDNS Script Configuration")
36+
37+
parser.add_argument("-u", "--username", help="one.com username", default=os.environ.get("ONECOM_USERNAME"))
38+
parser.add_argument("-p", "--password", help="one.com password", default=os.environ.get("ONECOM_PASSWORD"))
39+
env_onecome_domains = os.environ.get("ONECOM_DOMAINS")
40+
if env_onecome_domains is not None:
41+
env_onecome_domains = env_onecome_domains.split(',')
42+
43+
parser.add_argument("-d", "--domains", nargs="+", help="List of domain names (e.g.,-d example.com example2.com)", default=env_onecome_domains)
44+
parser.add_argument("-i", "--ip", help="IP address ('AUTO', or IP)", default=os.environ.get("ONECOM_IP", "AUTO"))
45+
46+
env_onecom_force = os.environ.get("ONECOM_FORCE_DNS_UPDATE")
47+
if env_onecom_force is not None:
48+
env_onecom_force = env_onecom_force.lower()
49+
50+
parser.add_argument("-f", "--force-update", action="store_true", help="Force DNS update (skip IP check)", default=env_onecom_force)
51+
parser.add_argument("-t", "--ttl", type=int, help="TTL value for DNS records", default=os.environ.get("ONECOM_TTL"))
52+
parser.add_argument("-y", "--skip-confirmation", action="store_true", help="Skip confirmation prompts", default=os.environ.get("ONECOM_SKIP_CONFIRMATION"))
53+
54+
args = parser.parse_args()
55+
56+
# Basic validation (ONLY IF validate_required is True)
57+
if validate_required:
58+
if not args.username:
59+
raise ValueError("Username is required (command-line or ONECOM_USERNAME env var)")
60+
if not args.password:
61+
raise ValueError("Password is required (command-line or ONECOM_PASSWORD env var)")
62+
if not args.domains:
63+
raise ValueError("Domain is required (command-line or ONECOM_DOMAIN env var)")
64+
65+
# Handle IP address retrieval
66+
if args.ip == "AUTO":
67+
try:
68+
args.ip = requests.get("https://api.ipify.org/").text
69+
except requests.ConnectionError:
70+
logger.error("Failed to get IP Address from ipify")
71+
raise SystemExit("Failed to get IP Address from ipify")
72+
logger.info(f"Detected external IP: {args.ip}")
73+
74+
return args

0 commit comments

Comments
 (0)