czds

- ICANN Centralized Zone Data Service Tool
git clone git://git.acid.vegas/czds.git
Log | Files | Refs | Archive | README | LICENSE

commit 5471e5cb971de7045204b4d0bd404f903aaa52b3
parent 01d1e6c4d83ef9f4a7c4784e6d5357287d92ead8
Author: acidvegas <acid.vegas@acid.vegas>
Date: Thu, 20 Feb 2025 23:16:31 -0500

Made into a class now for modular usage

Diffstat:
MLICENSE | 2+-
Mczds.py | 162+++++++++++++++++++++++++++++++++++++++++++------------------------------------
Dextras/service | 18------------------

3 files changed, 89 insertions(+), 93 deletions(-)

diff --git a/LICENSE b/LICENSE
@@ -1,6 +1,6 @@
 ISC License
 
-Copyright (c) 2024, acidvegas <acid.vegas@acid.vegas>
+Copyright (c) 2025, acidvegas <acid.vegas@acid.vegas>
 
 Permission to use, copy, modify, and/or distribute this software for any
 purpose with or without fee is hereby granted, provided that the above
diff --git a/czds.py b/czds.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
 # ICANN API for the Centralized Zones Data Service - developed by acidvegas (https://git.acid.vegas/czds)
 # Reference: https://czds.icann.org
 
@@ -12,87 +12,92 @@ import time
 import urllib.request
 
 
-# Setting up logging
-logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
+class CZDS:
+	'''Class for the ICANN Centralized Zones Data Service'''
 
+	def __init__(self, username: str, password: str):
+		self.headers = {'Authorization': f'Bearer {self.authenticate(username, password)}'}
 
-def authenticate(username: str, password: str) -> str:
-	'''
-	Authenticate with the ICANN API and return the access token.
 
-	:param username: ICANN Username
-	:param password: ICANN Password
-	'''
+	def authenticate(self, username: str, password: str) -> dict:
+		'''
+		Authenticate with the ICANN API and return the access token.
 
-	data	= json.dumps({'username': username, 'password': password}).encode('utf-8')
-	headers = {'Content-Type': 'application/json'}
-	request = urllib.request.Request('https://account-api.icann.org/api/authenticate', data=data, headers=headers)
+		:param username: ICANN Username
+		:param password: ICANN Password
+		'''
 
-	with urllib.request.urlopen(request) as response:
-		response = response.read().decode('utf-8')
-		return json.loads(response)['accessToken']
+		try:
+			# Prepare the request
+			data	= json.dumps({'username': username, 'password': password}).encode('utf-8')
+			headers = {'Content-Type': 'application/json'}
+			request = urllib.request.Request('https://account-api.icann.org/api/authenticate', data=data, headers=headers)
 
+			# Make the request
+			with urllib.request.urlopen(request) as response:
+				response = response.read().decode('utf-8')
+			
+			return json.loads(response)['accessToken']
 
-def fetch_zone_links(token: str) -> list:
-	'''
-	Fetch the list of zone files available for download.
+		except Exception as e:
+			raise Exception(f'Failed to authenticate with ICANN API: {e}')
 
-	:param token: ICANN access token
-	'''
 
-	headers = {'Authorization': f'Bearer {token}'}
-	request = urllib.request.Request('https://czds-api.icann.org/czds/downloads/links', headers=headers)
+	def fetch_zone_links(self) -> list:
+		'''
+		Fetch the list of zone files available for download.
 
-	with urllib.request.urlopen(request) as response:
-		if response.status == 200:
-			return json.loads(response.read().decode('utf-8'))
-		else:
-			raise Exception(f'Failed to fetch zone links: {response.status} {response.reason}')
+		:param token: ICANN access token
+		'''
 
+		request = urllib.request.Request('https://czds-api.icann.org/czds/downloads/links', headers=self.headers)
 
-def download_report(token: str, output_directory: str, username: str):
-	'''
-	Downloads the zone report stats from the API and scrubs the report for privacy.
+		with urllib.request.urlopen(request) as response:
+			if response.status == 200:
+				return json.loads(response.read().decode('utf-8'))
+			else:
+				raise Exception(f'Failed to fetch zone links: {response.status} {response.reason}')
 
-	:param token: ICANN access token
-	:param output_directory: Directory to save the scrubbed report
-	:param username: Username to be redacted
-	'''
 
-	filepath = os.path.join(output_directory, '.stats.csv')
-	headers  = {'Authorization': f'Bearer {token}'}
-	request  = urllib.request.Request('https://czds-api.icann.org/czds/requests/report', headers=headers)
+	def download_report(self, output_directory):
+		'''
+		Downloads the zone report stats from the API and scrubs the report for privacy.
+
+		:param token: ICANN access token
+		:param output_directory: Directory to save the scrubbed report
+		:param username: Username to be redacted
+		'''
+
+		filepath = os.path.join(output_directory, '.stats.csv')
+		request  = urllib.request.Request('https://czds-api.icann.org/czds/requests/report', headers=self.headers)
+
+		with urllib.request.urlopen(request) as response:
+			if not (response.status == 200):
+				raise Exception(f'Failed to download the zone stats report: {response.status} {response.reason}')
 
-	with urllib.request.urlopen(request) as response:
-		if response.status == 200:
 			report_data = response.read().decode('utf-8').replace(username, 'nobody@no.name')
 			with open(filepath, 'w') as file:
 				file.write(report_data)
-		else:
-			raise Exception(f'Failed to download the zone stats report: {response.status} {response.reason}')
 
 
+	def download_zone(self, url: str, output_directory: str):
+		'''
+		Download a single zone file using urllib.request.
 
-def download_zone(url: str, token: str, output_directory: str):
-	'''
-	Download a single zone file using urllib.request.
+		:param url: URL to download
+		:param output_directory: Directory to save the zone file
+		'''
 
-	:param url: URL to download
-	:param token: ICANN access token
-	:param output_directory: Directory to save the zone file
-	'''
+		request = urllib.request.Request(url, headers=self.headers)
 
-	headers = {'Authorization': f'Bearer {token}'}
-	request = urllib.request.Request(url, headers=headers)
+		with urllib.request.urlopen(request) as response:
+			if response.status != 200:
+				raise Exception(f'Failed to download {url}: {response.status} {response.reason}')
 
-	with urllib.request.urlopen(request) as response:
-		if response.status == 200:
-			content_disposition = response.getheader('Content-Disposition')
-			if content_disposition:
-				filename = content_disposition.split('filename=')[-1].strip('"')
-			else:
-				raise ValueError(f'Failed to get filename from Content-Disposition header: {content_disposition}')
+			if not (content_disposition := response.getheader('Content-Disposition')):
+				raise ValueError('Missing Content-Disposition header')
 
+			filename = content_disposition.split('filename=')[-1].strip('"')
 			filepath = os.path.join(output_directory, filename)
 
 			with open(filepath, 'wb') as file:
@@ -103,8 +108,6 @@ def download_zone(url: str, token: str, output_directory: str):
 					file.write(chunk)
 
 			return filepath
-		else:
-			raise Exception(f'Failed to download {url}: {response.status} {response.reason}')
 
 
 def main(username: str, password: str, concurrency: int):
@@ -119,31 +122,32 @@ def main(username: str, password: str, concurrency: int):
 	now = time.strftime('%Y-%m-%d')
 
 	logging.info(f'Authenticating with ICANN API...')
-	try:
-		token = authenticate(username, password)
-	except Exception as e:
-		raise Exception(f'Failed to authenticate with ICANN API: {e}')
-	#logging.info(f'Authenticated with token: {token}')
-	# The above line is commented out to avoid printing the token to the logs, you can uncomment it for debugging purposes
+	
+	CZDS_client = CZDS(username, password)
 
+	logging.debug('Created CZDS client')
+	
 	output_directory = os.path.join(os.getcwd(), 'zones', now)
 	os.makedirs(output_directory, exist_ok=True)
 
-	logging.info('Fetching zone stats report...')
+	logging.info('Fetching zone stats report...')	
+	
 	try:
-		download_report(token, output_directory, username)
+		CZDS_client.download_report(output_directory)
 	except Exception as e:
 		raise Exception(f'Failed to download zone stats report: {e}')
 
 	logging.info('Fetching zone links...')
+
 	try:
-		zone_links = fetch_zone_links(token)
+		zone_links = CZDS_client.fetch_zone_links()
 	except Exception as e:
 		raise Exception(f'Failed to fetch zone links: {e}')
-	logging.info(f'Fetched {len(zone_links)} zone links')
+	
+	logging.info(f'Fetched {len(zone_links):,} zone links')
 
 	with concurrent.futures.ThreadPoolExecutor(max_workers=concurrency) as executor:
-		future_to_url = {executor.submit(download_zone, url, token, output_directory): url for url in zone_links}
+		future_to_url = {executor.submit(CZDS_client.download_zone, url, output_directory): url for url in sorted(zone_links)}
 		for future in concurrent.futures.as_completed(future_to_url):
 			url = future_to_url[future]
 			try:
@@ -155,13 +159,23 @@ def main(username: str, password: str, concurrency: int):
 
 
 if __name__ == '__main__':
+	# Create argument parser
 	parser = argparse.ArgumentParser(description='ICANN API for the Centralized Zones Data Service')
-	parser.add_argument('-u', '--username', help='ICANN Username')
-	parser.add_argument('-p', '--password', help='ICANN Password')
+
+	# Add arguments
+	parser.add_argument('-u', '--username', default=os.getenv('CZDS_USER'), help='ICANN Username')
+	parser.add_argument('-p', '--password', default=os.getenv('CZDS_PASS'), help='ICANN Password')
 	parser.add_argument('-c', '--concurrency', type=int, default=3, help='Number of concurrent downloads')
+
+	# Parse arguments
 	args = parser.parse_args()
 
-	username = args.username or os.getenv('CZDS_USER') or input('ICANN Username: ')
-	password = args.password or os.getenv('CZDS_PASS') or getpass.getpass('ICANN Password: ')
+	# Setting up logging
+	logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
+
+	# Get username and password
+	username = args.username or input('ICANN Username: ')
+	password = args.password or getpass.getpass('ICANN Password: ')
 
+	# Execute main function
 	main(username, password, args.concurrency)
diff --git a/extras/service b/extras/service
@@ -1,17 +0,0 @@
-#!/bin/sh
-# systemd user service timer setup for czds - developed by acidvegas (https://git.acid.vegas/czds)
-# dont forget to export your CZDS_USER and CZDS_PASS before running
-
-CZDS='/path/to/czds'
-
-systemd_service() {
-    mkdir -p $HOME/.config/systemd/user
-    printf "[Unit]\nDescription=ICANN Centralized Zone Data Service (CZDS) Updater\n\n[Service]\nType=oneshot\nExecStart=$CZDS" > $HOME/.config/systemd/user/czds.service
-    printf "[Unit]\nDescription=Timer for ICANN Centralized Zone Data Service (CZDS) Updater\n\n[Timer]\nOnCalendar=monthly\nPersistent=true\n\n[Install]\nWantedBy=timers.target" > $HOME/.config/systemd/user/czds.timer
-    systemctl --user daemon-reload
-    systemctl --user enable czds.timer && systemctl --user start  czds.timer
-}
-
-cronjob() {
-    (crontab -l 2>/dev/null; echo "0 3 1 * * $CZDS") | crontab -
-}
-\ No newline at end of file