Skip to content

Commit e1514e0

Browse files
committed
Refactored script to use new modules functionality
1 parent ea0f584 commit e1514e0

File tree

2 files changed

+190
-169
lines changed

2 files changed

+190
-169
lines changed

modules/one_com_api.py

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
# modules/one_com_api.py
2+
import requests
3+
import json
4+
import tldextract
5+
import logging
6+
7+
logger = logging.getLogger("one_com_ddns")
8+
9+
def _find_between(haystack, needle1, needle2):
10+
index1 = haystack.find(needle1) + len(needle1)
11+
index2 = haystack.find(needle2, index1 + 1)
12+
return haystack[index1 : index2]
13+
14+
def login_session(username, password):
15+
logger.info("Logging in...")
16+
session = requests.session()
17+
redirect_url = "https://www.one.com/admin/"
18+
try:
19+
r = session.get(redirect_url)
20+
except requests.ConnectionError:
21+
logger.error("Connection to one.com failed.")
22+
raise SystemExit("Connection to one.com failed.")
23+
24+
post_url = _find_between(r.text, '<form id="kc-form-login" class="Login-form login autofill" onsubmit="login.disabled = true; return true;" action="', '"').replace('&amp;', '&')
25+
login_data = {'username': username, 'password': password, 'credentialId': ''}
26+
response = session.post(post_url, data=login_data)
27+
if "Invalid username or password." in response.text:
28+
logger.error("Invalid credentials. Exiting")
29+
exit(1)
30+
logger.info("Login successful.")
31+
return session
32+
33+
def select_admin_domain(session, domain):
34+
request_str = f"https://www.one.com/admin/select-admin-domain.do?domain={domain}"
35+
session.get(request_str)
36+
37+
def get_custom_records(session, domain):
38+
extracted = tldextract.extract(domain)
39+
primary_domain = f"{extracted.domain}.{extracted.suffix}"
40+
logger.info(f"Getting Records for primary domain: {primary_domain} (Requesting for: {domain})")
41+
dns_url = f"https://www.one.com/admin/api/domains/{primary_domain}/dns/custom_records"
42+
try:
43+
response = session.get(dns_url)
44+
response.raise_for_status()
45+
get_res = response.text
46+
if not get_res:
47+
raise ValueError("Empty response from API")
48+
return json.loads(get_res)["result"]["data"]
49+
except requests.exceptions.RequestException as e:
50+
logger.error(f"Error fetching DNS records for domain: {domain}. HTTP Status Code: {e.response.status_code if e.response else 'N/A'}")
51+
raise SystemExit(f"Failed to get DNS records for domain {domain}: {e}") from e
52+
except (json.JSONDecodeError, KeyError, TypeError) as e:
53+
logger.error(f"Error parsing JSON response for domain: {domain}. Response text was: {get_res}")
54+
raise SystemExit(f"Failed to parse DNS records JSON for domain {domain}: {e}") from e
55+
56+
def find_id_by_subdomain(records, subdomain):
57+
extracted_subdomain = tldextract.extract(subdomain).subdomain
58+
logger.info(f"searching domain prefix for: '{extracted_subdomain}'")
59+
for obj in records:
60+
if obj["attributes"]["prefix"] == extracted_subdomain:
61+
logger.info(f"Found Domain Prefix '{extracted_subdomain}': {obj['id']}")
62+
return obj
63+
return None
64+
65+
def change_ip(session, record, domain, ip, ttl):
66+
record_id = record["id"]
67+
current_ttl = record["attributes"]["ttl"]
68+
actual_ttl = ttl if ttl is not None else current_ttl
69+
extracted = tldextract.extract(domain)
70+
primary_domain = f"{extracted.domain}.{extracted.suffix}"
71+
subdomain = extracted.subdomain
72+
logger.info(f"Changing IP on record for subdomain '{subdomain}' - ID '{record_id}' TO NEW IP '{ip}' with TTL '{actual_ttl}' on primary domain '{primary_domain}'")
73+
to_send = {
74+
"type": "dns_service_records",
75+
"id": record_id,
76+
"attributes": {
77+
"type": "A",
78+
"prefix": subdomain,
79+
"content": ip,
80+
"ttl": actual_ttl
81+
}
82+
}
83+
dns_url = f"https://www.one.com/admin/api/domains/{primary_domain}/dns/custom_records/{record_id}"
84+
send_headers = {'Content-Type': 'application/json'}
85+
try:
86+
response = session.patch(dns_url, data=json.dumps(to_send), headers=send_headers)
87+
response.raise_for_status()
88+
logger.info("Sent Change IP Request")
89+
except requests.exceptions.RequestException as e:
90+
logger.error(f"Error updating IP for record with subdomain '{subdomain}': {e}")
91+
status_code = e.response.status_code if e.response else 'N/A'
92+
logger.error(f"HTTP Status Code: {status_code}")
93+
raise SystemExit(f"Failed to update IP for record with subdomain {subdomain}: {e}") from e

one_com_ddns.py

Lines changed: 97 additions & 169 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
'''
2-
32
#################################
4-
## ##
5-
## one.com DDNS Script ##
6-
## ##
3+
## ##
4+
## one.com DDNS Script ##
5+
## ##
76
#################################
8-
| Version | 2.4 |
7+
| Version | 2.4 |
98
+--------------+----------------+
10-
| Last Updated | 2023-10-05 |
9+
| Last Updated | 2023-10-05 |
1110
+--------------+----------------+
1211
1312
+----------------+-------------------------+
@@ -17,8 +16,6 @@
1716
+----------------+-------------------------+
1817
1918
20-
21-
2219
Note:
2320
This script is not very fail proof.
2421
Very few possible exceptions are handled, something as simple
@@ -27,168 +24,99 @@
2724
2825
If you have any problems or suggestions, please open an issue
2926
on github or send me an email (main@lugico.de)
30-
3127
'''
3228

29+
import modules.dns_utils as dns_utils
30+
import modules.one_com_config as config
31+
import modules.one_com_api as one_com_api
32+
import modules.logger as logger_module # Import the logger module
33+
import logging
34+
35+
logger = logger_module.setup_logging() # Setup logging
36+
37+
# #################################
38+
# ## ##
39+
# ## HARDCODED VALUES ##
40+
# ## ##
41+
# #################################
42+
# If you wish to hardcode values below and disable cli/env parsing set validate_required=False
43+
settings = config.parse_config(validate_required=True)
44+
45+
# ONE.COM LOGIN
46+
USERNAME = settings.username
47+
PASSWORD = settings.password
48+
49+
# YOUR DOMAIN (NOT www.example.com, www2.example.com)
50+
DOMAINS = settings.domains
51+
52+
# YOUR IP ADDRESS. DEFAULTS TO RESOLVING VIA ipify.org, set to 'ARG' to take from script argument
53+
IP = settings.ip
54+
55+
# FORCE UPDATE DNS RECORD
56+
FORCE_UPDATE = settings.force_update
57+
58+
# TTL VALUE FOR DNS RECORD
59+
TTL = settings.ttl
60+
61+
# SKIP CONFIRMATION
62+
SKIP_CONFIRMATION = settings.skip_confirmation
63+
64+
# Create login session - Session is created only once outside the domain loop
65+
s = one_com_api.login_session(USERNAME, PASSWORD)
66+
67+
# loop through list of domains
68+
for DOMAIN in DOMAINS:
69+
print()
70+
logger.info(f"Processing domain: {DOMAIN}")
71+
one_com_api.select_admin_domain(s, DOMAIN) # Select domain at the beginning of each domain loop
72+
73+
# get dns records for the current domain
74+
records = one_com_api.get_custom_records(s, DOMAIN)
75+
76+
# Check current IP from DNS
77+
logger.info(f"Attempting to get current DNS IP for: {DOMAIN}")
78+
current_dns_ip_info = dns_utils.get_ip_and_ttl(DOMAIN)
79+
80+
if current_dns_ip_info:
81+
current_dns_ip, current_ttl = current_dns_ip_info
82+
logger.info(f"Current DNS IP for {DOMAIN}: {current_dns_ip}, TTL: {current_ttl}")
83+
84+
if not FORCE_UPDATE and current_dns_ip == IP:
85+
logger.info(f"IP Address hasn't changed for {DOMAIN}. Aborting update for this domain.")
86+
continue
87+
88+
# change ip address
89+
record_obj = one_com_api.find_id_by_subdomain(records, DOMAIN)
90+
if record_obj is None:
91+
logger.error(f"Record '{DOMAIN}' could not be found.")
92+
continue
93+
94+
# Ask for confirmation before changing
95+
logger.warning(f"Changing IP for {DOMAIN} from {current_dns_ip} to {IP} with TTL {TTL}.")
96+
if settings.skip_confirmation:
97+
one_com_api.change_ip(s, record_obj, DOMAIN, IP, TTL)
98+
else:
99+
confirmation = input("Do you want to proceed? (y/n): ")
100+
if confirmation.lower() == 'y':
101+
one_com_api.change_ip(s, record_obj, DOMAIN, IP, TTL)
102+
else:
103+
logger.info(f"Update for {DOMAIN} cancelled.")
33104

34-
35-
# YOUR ONE.COM LOGIN
36-
USERNAME="email.address@example.com"
37-
PASSWORD="Your Beautiful Password"
38-
39-
# YOUR DOMAIN ( NOT www.example.com, only example.com )"
40-
DOMAIN="example.com"
41-
42-
# LIST OF SUBDOMAINS YOU WANT POINTING TO YOUR IP
43-
SUBDOMAINS = ["myddns"]
44-
# SUBDOMAINS = ["mutiple", "subdomains"]
45-
46-
47-
# YOUR IP ADDRESS.
48-
IP='AUTO'
49-
# '127.0.0.1' -> IP Address
50-
# 'AUTO' -> Automatically detect using ipify.org
51-
# 'ARG' -> Read from commandline argument ($ python3 ddns.py 127.0.0.1)
52-
53-
54-
# CHECK IF IP ADDRESS HAS CHANGED SINCE LAST SCRIPT EXECUTION?
55-
CHECK_IP_CHANGE = True
56-
# True = only continue when IP has changed
57-
# False = always continue
58-
59-
# PATH WHERE THE LAST IP SHOULD BE SAVED INBETWEEN SCRIPT EXECUTIONS
60-
# not needed CHECK_IP_CHANGE is false
61-
LAST_IP_FILE = "lastip.txt"
62-
63-
64-
import requests
65-
import json
66-
import sys
67-
68-
if IP == 'AUTO':
69-
print("Fetching IP Address...")
70-
try:
71-
IP = requests.get("https://api.ipify.org/").text
72-
except requests.ConnectionError:
73-
raise SystemExit("Failed to get IP Address from ipify")
74-
print(f"Detected IP: {IP}")
75-
elif IP == 'ARG':
76-
if (len(sys.argv) < 2):
77-
raise SystemExit('No IP Address provided in commandline arguments')
78105
else:
79-
IP = sys.argv[1]
80-
81-
if CHECK_IP_CHANGE:
82-
try:
83-
# try to read file
84-
with open(LAST_IP_FILE,"r") as f:
85-
if (IP == f.read()):
86-
# abort if ip in file is same as current
87-
print("IP Address hasn't changed. Aborting")
88-
exit()
89-
except IOError:
90-
pass
91-
92-
# write current ip to file
93-
with open(LAST_IP_FILE,"w") as f:
94-
f.write(IP)
95-
96-
97-
def findBetween(haystack, needle1, needle2):
98-
index1 = haystack.find(needle1) + len(needle1)
99-
index2 = haystack.find(needle2, index1 + 1)
100-
return haystack[index1 : index2]
101-
102-
103-
# will create a requests session and log you into your one.com account in that session
104-
def loginSession(USERNAME, PASSWORD, TARGET_DOMAIN=''):
105-
print("Logging in...")
106-
107-
# create requests session
108-
session = requests.session()
109-
110-
# get admin panel to be redirected to login page
111-
redirectmeurl = "https://www.one.com/admin/"
112-
try:
113-
r = session.get(redirectmeurl)
114-
except requests.ConnectionError:
115-
raise SystemExit("Connection to one.com failed.")
116-
117-
# find url to post login credentials to from form action attribute
118-
substrstart = '<form id="kc-form-login" class="Login-form login autofill" onsubmit="login.disabled = true; return true;" action="'
119-
substrend = '"'
120-
posturl = findBetween(r.text, substrstart, substrend).replace('&amp;','&')
121-
122-
# post login data
123-
logindata = {'username': USERNAME, 'password': PASSWORD, 'credentialId' : ''}
124-
response = session.post(posturl, data=logindata)
125-
if response.text.find("Invalid username or password.") != -1:
126-
print("!!! - Invalid credentials. Exiting")
127-
exit(1)
128-
129-
print("Login successful.")
130-
131-
# For accounts with multiple domains it seems to still be needed to select which target domain to operate on.
132-
if TARGET_DOMAIN:
133-
print("Setting active domain to: {}".format(TARGET_DOMAIN))
134-
selectAdminDomain(session, TARGET_DOMAIN)
135-
136-
return session
137-
138-
139-
def selectAdminDomain(session, DOMAIN):
140-
request_str = "https://www.one.com/admin/select-admin-domain.do?domain={}".format(DOMAIN)
141-
session.get(request_str)
142-
143-
144-
# gets all DNS records on your domain.
145-
def getCustomRecords(session, DOMAIN):
146-
print("Getting Records")
147-
getres = session.get("https://www.one.com/admin/api/domains/" + DOMAIN + "/dns/custom_records").text
148-
if len(getres) == 0:
149-
print("!!! - No records found. Exiting")
150-
exit()
151-
return json.loads(getres)["result"]["data"]
152-
153-
154-
# finds the record id of a record from it's subdomain
155-
def findIdBySubdomain(records, subdomain):
156-
print("searching domain '" + subdomain + "'")
157-
for obj in records:
158-
if obj["attributes"]["prefix"] == subdomain:
159-
print("Found Domain '" + subdomain + "': " + obj["id"])
160-
return obj["id"]
161-
return ""
162-
163-
164-
# changes the IP Address of a TYPE A record. Default TTL=3800
165-
def changeIP(session, ID, DOMAIN, SUBDOMAIN, IP, TTL=3600):
166-
print("Changing IP on subdomain '" + SUBDOMAIN + "' - ID '" + ID + "' TO NEW IP '" + IP + "'")
167-
168-
tosend = {"type":"dns_service_records","id":ID,"attributes":{"type":"A","prefix":SUBDOMAIN,"content":IP,"ttl":TTL}}
169-
170-
dnsurl="https://www.one.com/admin/api/domains/" + DOMAIN + "/dns/custom_records/" + ID
171-
172-
sendheaders={'Content-Type': 'application/json'}
173-
174-
session.patch(dnsurl, data=json.dumps(tosend), headers=sendheaders)
175-
176-
print("Sent Change IP Request")
177-
178-
179-
180-
# Create login session
181-
s = loginSession(USERNAME, PASSWORD, DOMAIN)
182-
183-
# get dns records
184-
records = getCustomRecords(s, DOMAIN)
185-
#print(records)
186-
187-
# loop through list of subdomains
188-
for subdomain in SUBDOMAINS:
189-
#change ip address
190-
recordid = findIdBySubdomain(records, subdomain)
191-
if recordid == "":
192-
print("!!! - Record '" + subdomain + "' could not be found.")
193-
continue
194-
changeIP(s, recordid, DOMAIN, subdomain, IP, 600)
106+
logger.warning(f"Could not retrieve current DNS IP for {DOMAIN} after multiple retries. Proceeding with update anyway.")
107+
record_obj = one_com_api.find_id_by_subdomain(records, DOMAIN)
108+
if record_obj is None:
109+
logger.error(f"Record '{DOMAIN}' could not be found.")
110+
continue
111+
112+
logger.info(f"Changing IP for {DOMAIN} to {IP} with TTL {TTL}.")
113+
if settings.skip_confirmation:
114+
one_com_api.change_ip(s, record_obj, DOMAIN, IP, TTL)
115+
else:
116+
confirmation = input("Do you want to proceed? (y/n): ")
117+
if confirmation.lower() == 'y':
118+
one_com_api.change_ip(s, record_obj, DOMAIN, IP, TTL)
119+
else:
120+
logger.info(f"Update for {DOMAIN} cancelled.")
121+
122+
logger.info("DDNS update process completed.")

0 commit comments

Comments
 (0)