From 3ae5a865fef02cafa156aef4de80cd86684b083e Mon Sep 17 00:00:00 2001 From: Walter Oggioni Date: Mon, 31 Mar 2025 03:59:26 +0800 Subject: [PATCH] added Cloudflare API support --- .gitea/workflows/build.yaml | 2 +- {awsdyndns => dyndns}/__init__.py | 0 awsdyndns/wrapper.py => dyndns/aws.py | 9 +- dyndns/cloudflare.py | 80 +++++++++++++++ dyndns/utils.py | 7 ++ pyproject.toml | 25 +++-- requirements.txt | 141 +++++++++++++++++++++----- 7 files changed, 225 insertions(+), 39 deletions(-) rename {awsdyndns => dyndns}/__init__.py (100%) rename awsdyndns/wrapper.py => dyndns/aws.py (85%) create mode 100644 dyndns/cloudflare.py create mode 100644 dyndns/utils.py diff --git a/.gitea/workflows/build.yaml b/.gitea/workflows/build.yaml index 7f0c0e1..28943ef 100644 --- a/.gitea/workflows/build.yaml +++ b/.gitea/workflows/build.yaml @@ -4,7 +4,7 @@ on: branches: [ master ] jobs: build: - runs-on: woryzen + runs-on: hostinger steps: - name: Checkout sources uses: actions/checkout@v4 diff --git a/awsdyndns/__init__.py b/dyndns/__init__.py similarity index 100% rename from awsdyndns/__init__.py rename to dyndns/__init__.py diff --git a/awsdyndns/wrapper.py b/dyndns/aws.py similarity index 85% rename from awsdyndns/wrapper.py rename to dyndns/aws.py index 92e1d5a..f16adfc 100755 --- a/awsdyndns/wrapper.py +++ b/dyndns/aws.py @@ -1,10 +1,8 @@ -#!/usr/bin/env python3 - import json -from urllib.request import urlopen, Request import argparse from datetime import datetime from subprocess import check_call +from .utils import get_public_ip def run(): parser = argparse.ArgumentParser() @@ -19,11 +17,8 @@ def run(): args = parser.parse_args() - url = "http://v4.ipv6-test.com/api/myip.php" - request = Request(url) - with urlopen(request) as request: - public_ip = request.read().decode() date = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + public_ip = get_public_ip() params = { 'Comment': f'Auto update by awsdyndns on {date}', 'Changes': [ diff --git a/dyndns/cloudflare.py b/dyndns/cloudflare.py new file mode 100644 index 0000000..4b4c36f --- /dev/null +++ b/dyndns/cloudflare.py @@ -0,0 +1,80 @@ +import json +import argparse +from os import environ +from .utils import get_public_ip +from urllib.request import urlopen, Request + +# Define your variables +cloudflare_api_key = environ['CLOUDFLARE_API_KEY'] + +def update_dns_record( + zone_id: str, + dns_record_id: str, + ip_address: str, + domain_name: str, + ttl: int, + record_type : str, + proxied : bool = False, + comment : str = None +): + # Set the URL + url = f'https://api.cloudflare.com/client/v4/zones/{zone_id}/dns_records/{dns_record_id}' + + # Set the headers + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {cloudflare_api_key}' + } + + # Define the data to be sent in the PATCH request + data = { + "comment": comment, + "content": ip_address, + "name": domain_name, + "proxied": proxied, + "ttl": ttl, + "type": record_type + } + + # Make the PATCH request + request = Request(url, headers=headers, data=json.dumps(data).encode('UTF-8'), method='PATCH') + with urlopen(request) as request: + if request.status != 200: + response = request.read().decode() + msg = f'Failed to update DNS record: {request.status} {response}' + raise RuntimeError(msg) + + +def run(): + parser = argparse.ArgumentParser() + parser.add_argument('-z', "--hosted-zone", metavar='HOSTED_ZONE_ID', required=True, + help="Specify the hosted zone id") + parser.add_argument('-d', "--domain", metavar='DOMAIN_NAME', required=True, + help="Specify domain name") + parser.add_argument('-t', "--type", metavar='DNS_TYPE', default='A', + help="Specify DNS type") + parser.add_argument('-l', "--ttl", metavar='TTL_S', type=int, default=300, + help="Specify time-to-live in seconds") + parser.add_argument('-r', "--record-id", metavar='RECORD_ID', + help="Cloudflare DNS record id") + parser.add_argument('-p', "--proxied", metavar='PROXIED', type=bool, default=False, + help="Whether or not the traffic will be proxied through Cloudflare network") + parser.add_argument('-c', "--comment", metavar='COMMENT', help="Specify a comment for this entry") + + args = parser.parse_args() + + public_ip = get_public_ip() + + update_dns_record( + zone_id=args.hosted_zone, + domain_name=args.domain, + record_type=args.type, + ip_address=public_ip, + ttl=args.ttl, + dns_record_id=args.record_id, + proxied=args.proxied, + comment=args.comment + ) + + + diff --git a/dyndns/utils.py b/dyndns/utils.py new file mode 100644 index 0000000..2ae2932 --- /dev/null +++ b/dyndns/utils.py @@ -0,0 +1,7 @@ +from urllib.request import urlopen, Request + +def get_public_ip() -> str: + url = "http://v4.ipv6-test.com/api/myip.php" + request = Request(url) + with urlopen(request) as request: + return request.read().decode() \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 4d0a8ed..cb63526 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,8 +3,8 @@ requires = ["setuptools>=61.0"] build-backend = "setuptools.build_meta" [project] -name = "awsdyndns" -version = "0.2.0" +name = "dyndns" +version = "0.3.0" authors = [ { name="Walter Oggioni", email="oggioni.walter@gmail.com" }, ] @@ -16,14 +16,25 @@ classifiers = [ "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", ] -dependencies = [] +dependencies = [ +] + +[project.optional-dependencies] +dev = [ + "build", "mypy", "ipdb", "twine" +] + +[tool.setuptools.package-data] +dyndns = ['conf/*'] + [project.urls] -"Homepage" = "https://woggioni.net/cgit/awsdyndns.git/" -"Bug Tracker" = "https://woggioni.net/cgit/awsdyndns.git/" +"Homepage" = "https://gitea.woggioni.net/woggioni/dyndns/" +"Bug Tracker" = "https://gitea.woggioni.net/woggioni/dyndns/" [project.scripts] -awsdyndns = "awsdyndns.wrapper:run" +awsdyndns = "dyndns.aws:run" +cloudflaredyndns = "dyndns.cloudflare:run" [tool.mypy] python_version = "3.11" @@ -33,4 +44,4 @@ no_implicit_optional = true warn_return_any = true warn_unused_ignores = true exclude = ["scripts", "docs", "test"] -strict = true \ No newline at end of file +strict = true diff --git a/requirements.txt b/requirements.txt index 618c9bf..482845d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,31 +1,124 @@ -build==1.2.1 -certifi==2024.7.4 -cffi==1.16.0 -charset-normalizer==3.3.2 -cryptography==43.0.0 +# +# This file is autogenerated by pip-compile with Python 3.13 +# by the following command: +# +# pip-compile --allow-unsafe --extra=dev --output-file=requirements.txt pyproject.toml +# +--index-url https://gitea.woggioni.net/api/packages/woggioni/pypi/simple +--extra-index-url https://pypi.org/simple + +asttokens==3.0.0 + # via stack-data +build==1.2.2.post1 + # via dyndns (pyproject.toml) +certifi==2025.1.31 + # via requests +cffi==1.17.1 + # via cryptography +charset-normalizer==3.4.1 + # via requests +cryptography==44.0.2 + # via secretstorage +decorator==5.2.1 + # via + # ipdb + # ipython docutils==0.21.2 -idna==3.7 -importlib_metadata==8.2.0 -jaraco.classes==3.4.0 -jaraco.context==5.3.0 -jaraco.functools==4.0.1 -jeepney==0.8.0 -keyring==25.2.1 + # via readme-renderer +executing==2.2.0 + # via stack-data +id==1.5.0 + # via twine +idna==3.10 + # via requests +ipdb==0.13.13 + # via dyndns (pyproject.toml) +ipython==9.0.2 + # via ipdb +ipython-pygments-lexers==1.1.1 + # via ipython +jaraco-classes==3.4.0 + # via keyring +jaraco-context==6.0.1 + # via keyring +jaraco-functools==4.1.0 + # via keyring +jedi==0.19.2 + # via ipython +jeepney==0.9.0 + # via + # keyring + # secretstorage +keyring==25.6.0 + # via twine markdown-it-py==3.0.0 + # via rich +matplotlib-inline==0.1.7 + # via ipython mdurl==0.1.2 -more-itertools==10.3.0 -nh3==0.2.18 -packaging==24.1 -pkginfo==1.10.0 + # via markdown-it-py +more-itertools==10.6.0 + # via + # jaraco-classes + # jaraco-functools +mypy==1.15.0 + # via dyndns (pyproject.toml) +mypy-extensions==1.0.0 + # via mypy +nh3==0.2.21 + # via readme-renderer +packaging==24.2 + # via + # build + # twine +parso==0.8.4 + # via jedi +pexpect==4.9.0 + # via ipython +prompt-toolkit==3.0.50 + # via ipython +ptyprocess==0.7.0 + # via pexpect +pure-eval==0.2.3 + # via stack-data pycparser==2.22 -Pygments==2.18.0 -pyproject_hooks==1.1.0 -readme_renderer==44.0 + # via cffi +pygments==2.19.1 + # via + # ipython + # ipython-pygments-lexers + # readme-renderer + # rich +pyproject-hooks==1.2.0 + # via build +readme-renderer==44.0 + # via twine requests==2.32.3 + # via + # id + # requests-toolbelt + # twine requests-toolbelt==1.0.0 + # via twine rfc3986==2.0.0 -rich==13.7.1 -SecretStorage==3.3.3 -twine==5.1.1 -urllib3==2.2.2 -zipp==3.19.2 + # via twine +rich==14.0.0 + # via twine +secretstorage==3.3.3 + # via keyring +stack-data==0.6.3 + # via ipython +traitlets==5.14.3 + # via + # ipython + # matplotlib-inline +twine==6.1.0 + # via dyndns (pyproject.toml) +typing-extensions==4.13.0 + # via mypy +urllib3==2.3.0 + # via + # requests + # twine +wcwidth==0.2.13 + # via prompt-toolkit