diff --git a/app.py b/app.py index 9dc458c..9ad8b46 100644 --- a/app.py +++ b/app.py @@ -1,7 +1,6 @@ import base64 import os import re -import requests import urllib3 from concurrent.futures import Future, ThreadPoolExecutor, wait as gather_futures @@ -12,18 +11,34 @@ from flask import Flask, request NODE_NAME = (os.getenv('NODE_NAME') or '') + "_traefik" TRAEFIK_INSTANCE = os.getenv('TRAEFIK_INSTANCE') or '' TRAEFIK_HOST = os.getenv('TRAEFIK_HOST') or '' +EXTERNAL_HOST = os.getenv('EXTERNAL_HOST') or '' + INTERNAL_DESTS = (os.getenv('INTERNAL_DESTS') or '').split(',') or [] EXTERNAL_DESTS = (os.getenv('EXTERNAL_DESTS') or '').split(',') or [] + PRIVATE_KEY = os.getenv('PRIVATE_KEY') or '' +CLOUDFLARE_API_KEY = os.getenv('CLOUDFLARE_API_KEY') or '' +CLOUDFLARE_ZONE_ID = os.getenv('CLOUDFLARE_ZONE_ID') or '' + HOST = os.getenv('HOST') or '0.0.0.0' PORT = os.getenv('PORT') or '80' assert NODE_NAME != '_traefik' -assert TRAEFIK_INSTANCE -assert TRAEFIK_HOST -assert INTERNAL_DESTS -assert EXTERNAL_DESTS -assert PRIVATE_KEY +for x in [ + TRAEFIK_INSTANCE, + TRAEFIK_HOST, + EXTERNAL_HOST, + + INTERNAL_DESTS, + EXTERNAL_DESTS, + + PRIVATE_KEY, + CLOUDFLARE_API_KEY, + CLOUDFLARE_ZONE_ID, + + HOST, + PORT, +]: assert x app = Flask(__name__) PATTERN = re.compile(r"Host\(`((?:[a-zA-Z0-9_-]+\.)*wzray\.com)`\)") @@ -42,6 +57,14 @@ def Connection(host: str, user: str = 'root', port = None) -> fabric.Connection: {'auth_strategy': paramiko.auth_strategy.InMemoryPrivateKey(user, privkey)}) +def cf_request(method: str, target: str, json = None): + r = urllib3.request(method, 'https://api.cloudflare.com/client/v4/' + target, + headers={'Authorization': f'Bearer {CLOUDFLARE_API_KEY}'}, json=json) + if r.status != 200: + raise Exception(f'Error {r.status}: {r.data.decode()}') + return r.json()['result'] + + class Observer: internal: set[str] = set() external: set[str] = set() @@ -111,10 +134,34 @@ class Observer: return [cls.executor.submit(_update, x) for x in EXTERNAL_DESTS] + @classmethod + def update_cloudflare(cls): + url = f'zones/{CLOUDFLARE_ZONE_ID}/dns_records' + to_delete = [{'id': x['id']} for x in filter( + lambda x: x['comment'] == NODE_NAME, cf_request('GET', url))] + if not cls.external and not to_delete: + return + cf_request('POST', url + '/batch', { + 'deletes': to_delete, + 'posts': [{ + 'comment': NODE_NAME, + 'content': EXTERNAL_HOST, + 'type': 'A', + 'proxied': True, + 'name': x, + + } for x in cls.external] + }) + + @classmethod def update(cls, data): def _job(): - gather_futures(cls.update_external() + cls.update_internal()) + gather_futures([ + *cls.update_external(), + *cls.update_internal(), + cls.executor.submit(cls.update_cloudflare) + ]) cls.job = None cls.internal, cls.external = cls.parse_raw_data(data) print(f"{cls.internal=}, {cls.external=}") @@ -126,7 +173,10 @@ class Observer: @classmethod def parse_raw_data(cls, data: dict): http = data.get('http') or {} - flt = lambda ep: set([x.group(1) for x in [re.match(PATTERN, x['rule']) for x in http['routers'].values() if ep in x['entryPoints']] if x]) + + flt = lambda ep: set([z + for y in [re.findall(PATTERN, x['rule']) + for x in http['routers'].values() if ep in x['entryPoints']] if y for z in y]) internal = flt(INTERNAL_ENTRYPOINT) external = flt(EXTERNAL_ENTRYPOINT)