Skip to content

Commit a44a17b

Browse files
authored
Merge pull request #1 from M4RC0Sx/develop
Develop
2 parents d1ae7ac + f41972c commit a44a17b

File tree

13 files changed

+762
-0
lines changed

13 files changed

+762
-0
lines changed

.dockerignore

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
# Byte-compiled / optimized / DLL files
2+
__pycache__/
3+
*.py[cod]
4+
*$py.class
5+
6+
# C extensions
7+
*.so
8+
9+
# Distribution / packaging
10+
.Python
11+
build/
12+
develop-eggs/
13+
dist/
14+
downloads/
15+
eggs/
16+
.eggs/
17+
lib/
18+
lib64/
19+
parts/
20+
sdist/
21+
var/
22+
wheels/
23+
share/python-wheels/
24+
*.egg-info/
25+
.installed.cfg
26+
*.egg
27+
MANIFEST
28+
29+
# PyInstaller
30+
# Usually these files are written by a python script from a template
31+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
32+
*.manifest
33+
*.spec
34+
35+
# Installer logs
36+
pip-log.txt
37+
pip-delete-this-directory.txt
38+
39+
# Unit test / coverage reports
40+
htmlcov/
41+
.tox/
42+
.nox/
43+
.coverage
44+
.coverage.*
45+
.cache
46+
nosetests.xml
47+
coverage.xml
48+
*.cover
49+
*.py,cover
50+
.hypothesis/
51+
.pytest_cache/
52+
cover/
53+
54+
# Translations
55+
*.mo
56+
*.pot
57+
58+
# Django stuff:
59+
*.log
60+
local_settings.py
61+
db.sqlite3
62+
db.sqlite3-journal
63+
64+
# Flask stuff:
65+
instance/
66+
.webassets-cache
67+
68+
# Scrapy stuff:
69+
.scrapy
70+
71+
# Sphinx documentation
72+
docs/_build/
73+
74+
# PyBuilder
75+
.pybuilder/
76+
target/
77+
78+
# Jupyter Notebook
79+
.ipynb_checkpoints
80+
81+
# IPython
82+
profile_default/
83+
ipython_config.py
84+
85+
# pyenv
86+
# For a library or package, you might want to ignore these files since the code is
87+
# intended to run in multiple environments; otherwise, check them in:
88+
# .python-version
89+
90+
# pipenv
91+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
93+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
94+
# install all needed dependencies.
95+
#Pipfile.lock
96+
97+
# poetry
98+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
99+
# This is especially recommended for binary packages to ensure reproducibility, and is more
100+
# commonly ignored for libraries.
101+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
102+
#poetry.lock
103+
104+
# pdm
105+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
106+
#pdm.lock
107+
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
108+
# in version control.
109+
# https://pdm.fming.dev/#use-with-ide
110+
.pdm.toml
111+
112+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
113+
__pypackages__/
114+
115+
# Celery stuff
116+
celerybeat-schedule
117+
celerybeat.pid
118+
119+
# SageMath parsed files
120+
*.sage.py
121+
122+
# Environments
123+
.env
124+
.venv
125+
env/
126+
venv/
127+
ENV/
128+
env.bak/
129+
venv.bak/
130+
131+
# Spyder project settings
132+
.spyderproject
133+
.spyproject
134+
135+
# Rope project settings
136+
.ropeproject
137+
138+
# mkdocs documentation
139+
/site
140+
141+
# mypy
142+
.mypy_cache/
143+
.dmypy.json
144+
dmypy.json
145+
146+
# Pyre type checker
147+
.pyre/
148+
149+
# pytype static type analyzer
150+
.pytype/
151+
152+
# Cython debug symbols
153+
cython_debug/
154+
155+
# PyCharm
156+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
157+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
158+
# and can be added to the global gitignore or merged into this file. For a more nuclear
159+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
160+
#.idea/

.env.example

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
PUBLIC_IP_PROVIDER=https://api.ipify.org
2+
REFRESH_MINUTES=5
3+
CF_API_URL=https://api.cloudflare.com/client/v4
4+
CF_API_EMAIL=youremail@gmail.com
5+
CF_API_KEY=cf_global_api_key
6+
CF_ZONE=yourdomain.com
7+
CF_RECORD=subdomain.yourdomain.com
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
name: semantic-release
2+
3+
on:
4+
push:
5+
branches:
6+
- master
7+
8+
jobs:
9+
release:
10+
runs-on: ubuntu-latest
11+
steps:
12+
- name: Checkout
13+
uses: actions/checkout@v3
14+
with:
15+
fetch-depth: 0
16+
- name: Setup Node.js
17+
uses: actions/setup-node@v4
18+
with:
19+
node-version: 20.10.0
20+
- name: Install semantic-release
21+
run: npm install -g semantic-release @semantic-release/changelog conventional-changelog-conventionalcommits @semantic-release/git semantic-release-replace-plugin @codedependant/semantic-release-docker
22+
- name: Docker login
23+
uses: docker/login-action@v3
24+
with:
25+
username: ${{ secrets.DOCKER_USERNAME }}
26+
password: ${{ secrets.DOCKER_PASSWORD }}
27+
- name: Publish
28+
env:
29+
GH_TOKEN: ${{ secrets.GH_TOKEN }}
30+
run: npx semantic-release

Dockerfile

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
FROM python:3.12-bullseye AS builder
2+
3+
RUN pip install poetry==1.7.1
4+
5+
ENV POETRY_NO_INTERACTION=1 \
6+
POETRY_VIRTUALENVS_IN_PROJECT=1 \
7+
POETRY_VIRTUALENVS_IN_PROJECT=1 \
8+
POETRY_CACHE_DIR='/tmp/poetry'
9+
10+
WORKDIR /app
11+
12+
COPY pyproject.toml poetry.lock ./
13+
RUN touch README.md
14+
RUN poetry install --no-dev --no-root
15+
RUN rm -rf ${POETRY_CACHE_DIR}
16+
17+
18+
FROM python:3.12-slim-bullseye AS runtime
19+
WORKDIR /app
20+
21+
ENV VIRTUAL_ENV=/app/.venv \
22+
PATH="/app/.venv/bin:$PATH"
23+
24+
COPY --from=builder ${VIRTUAL_ENV} ${VIRTUAL_ENV}
25+
COPY clouddnsflare /app/clouddnsflare
26+
27+
ENTRYPOINT ["python", "-m", "clouddnsflare"]
28+
29+

README.md

Whitespace-only changes.

clouddnsflare/__init__.py

Whitespace-only changes.

clouddnsflare/__main__.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
from os import getenv
2+
import sys
3+
import logging
4+
from time import sleep
5+
6+
7+
from clouddnsflare.defaults import DefaultConfig
8+
from clouddnsflare.cloudflare_api import CloudflareAPI
9+
10+
11+
logging.basicConfig(
12+
level=logging.INFO,
13+
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
14+
)
15+
logging.getLogger().addHandler(logging.StreamHandler())
16+
17+
18+
PUBLIC_IP_PROVIDER = getenv(
19+
"PUBLIC_IP_PROVIDER", DefaultConfig.PUBLIC_IP_PROVIDER.value
20+
)
21+
REFRESH_MINUTES = int(getenv("REFRESH_MINUTES", DefaultConfig.REFRESH_MINUTES.value))
22+
CF_API_URL = getenv("CF_API_URL", DefaultConfig.CF_API_URL.value)
23+
CF_API_EMAIL = getenv("CF_API_EMAIL", DefaultConfig.CF_API_EMAIL.value)
24+
CF_API_KEY = getenv("CF_API_KEY", DefaultConfig.CF_API_KEY.value)
25+
CF_ZONE = getenv("CF_ZONE", DefaultConfig.CF_ZONE.value)
26+
CF_RECORD = getenv("CF_RECORD", DefaultConfig.CF_RECORD.value)
27+
28+
29+
def main() -> None:
30+
logging.info("ClouDDNSflare by M4RC0Sx (https://github.com/M4RC0Sx) is starting...")
31+
logging.info(f"Public IP provider: {PUBLIC_IP_PROVIDER}")
32+
logging.info(f"Refresh minutes: {REFRESH_MINUTES}")
33+
logging.info(f"Cloudflare zone: {CF_ZONE}")
34+
logging.info(f"Cloudflare record: {CF_RECORD}")
35+
36+
cf_api = CloudflareAPI(
37+
email=CF_API_EMAIL,
38+
key=CF_API_KEY,
39+
zone=CF_ZONE,
40+
record=CF_RECORD,
41+
cf_api=CF_API_URL,
42+
public_ip_provider=PUBLIC_IP_PROVIDER,
43+
)
44+
45+
while True:
46+
logging.info("Refreshing public IP...")
47+
48+
try:
49+
cf_api.update_dns_record()
50+
except Exception as e:
51+
logging.error(f"There was an error updating DNS record: {e}. Exiting...")
52+
sys.exit(1)
53+
54+
logging.info(f"Waiting {REFRESH_MINUTES} minutes before next refresh...")
55+
sleep(REFRESH_MINUTES * 60)
56+
57+
58+
if __name__ == "__main__":
59+
main()

clouddnsflare/cloudflare_api.py

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import logging
2+
3+
import requests
4+
5+
6+
class CloudflareAPI:
7+
def __init__(
8+
self,
9+
email: str,
10+
key: str,
11+
zone: str,
12+
record: str,
13+
cf_api: str,
14+
public_ip_provider: str,
15+
) -> None:
16+
self.email = email
17+
self.key = key
18+
self.zone = zone
19+
self.record = record
20+
self.cf_api = cf_api
21+
22+
self.public_ip_provider = public_ip_provider
23+
24+
self.zone_id: str | None = None
25+
self.record_id: str | None = None
26+
27+
def __get_cf_headers(self) -> dict[str, str]:
28+
return {
29+
"X-Auth-Email": self.email,
30+
"X-Auth-Key": self.key,
31+
"Content-Type": "application/json",
32+
}
33+
34+
def __get_public_ip(self) -> str:
35+
return requests.get(self.public_ip_provider).text
36+
37+
def __get_zone_id(self) -> str:
38+
if self.zone_id:
39+
return self.zone_id
40+
41+
url = f"{self.cf_api}/zones?name={self.zone}"
42+
headers = self.__get_cf_headers()
43+
response = requests.get(url, headers=headers)
44+
return str(response.json()["result"][0]["id"])
45+
46+
def __get_record_id(self, zone_id: str) -> str:
47+
if self.record_id:
48+
return self.record_id
49+
50+
url = f"{self.cf_api}/zones/{zone_id}/dns_records"
51+
headers = self.__get_cf_headers()
52+
params = {"name": self.record, "type": "A"}
53+
response = requests.get(url, headers=headers, params=params)
54+
return str(response.json()["result"][0]["id"])
55+
56+
def update_dns_record(self) -> None:
57+
zone_id = None
58+
record_id = None
59+
public_ip = None
60+
61+
try:
62+
zone_id = self.__get_zone_id()
63+
except Exception as e:
64+
logging.error(f"Error getting zone ID: {e}")
65+
raise e
66+
self.zone_id = zone_id
67+
try:
68+
record_id = self.__get_record_id(zone_id)
69+
except Exception as e:
70+
logging.error(f"Error getting record ID: {e}")
71+
raise e
72+
self.record_id = record_id
73+
try:
74+
public_ip = self.__get_public_ip()
75+
except Exception as e:
76+
logging.error(f"Error getting public IP: {e}")
77+
raise e
78+
79+
url = f"{self.cf_api}/zones/{zone_id}/dns_records/{record_id}"
80+
headers = self.__get_cf_headers()
81+
data = {
82+
"type": "A",
83+
"name": self.record,
84+
"content": public_ip,
85+
"proxied": False,
86+
}
87+
requests.put(url, headers=headers, json=data)
88+
logging.info(f"DNS record updated to {public_ip}")

clouddnsflare/defaults.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
from enum import Enum
2+
3+
4+
class DefaultConfig(Enum):
5+
# Public IP service provider
6+
PUBLIC_IP_PROVIDER = "https://api.ipify.org"
7+
# Refresh minutes
8+
REFRESH_MINUTES = 5
9+
# Cloudflare API URL
10+
CF_API_URL = "https://api.cloudflare.com/client/v4"
11+
# Cloudflare API email
12+
CF_API_EMAIL = ""
13+
# Cloudflare API key
14+
CF_API_KEY = ""
15+
# Cloudflare zone
16+
CF_ZONE = ""
17+
# Cloudflare record
18+
CF_RECORD = ""

0 commit comments

Comments
 (0)