commit
27ce46fa76
@ -0,0 +1,21 @@ |
||||
MIT License |
||||
|
||||
Copyright (c) 2020 Gonçalo Valério |
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy |
||||
of this software and associated documentation files (the "Software"), to deal |
||||
in the Software without restriction, including without limitation the rights |
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
||||
copies of the Software, and to permit persons to whom the Software is |
||||
furnished to do so, subject to the following conditions: |
||||
|
||||
The above copyright notice and this permission notice shall be included in all |
||||
copies or substantial portions of the Software. |
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
||||
SOFTWARE. |
@ -0,0 +1,8 @@ |
||||
# Worker DDNS |
||||
|
||||
Workers as a proxy in a DDNS setup that uses the cloudflare API. |
||||
|
||||
Both scripts, `worker.js` and `agent.py` don't require any extra dependencies, |
||||
so it is very simple to deploy/setup. Just copy the files and you're done. |
||||
|
||||
More detailed documentation will be added soon. |
@ -0,0 +1,78 @@ |
||||
#!/usr/bin/env python3 |
||||
""" |
||||
Simple agent script that collects the public IP address of the machine it is |
||||
running on and then updates a Cloudflare Worker. |
||||
|
||||
All requests are signed using a pre-shared key to ensure the integrity of the |
||||
message and authenticate the source. |
||||
""" |
||||
import os |
||||
import sys |
||||
import hmac |
||||
import json |
||||
import logging |
||||
import random |
||||
from datetime import datetime |
||||
from urllib import request, parse, error |
||||
|
||||
|
||||
logger = logging.getLogger(__name__) |
||||
|
||||
IP_SOURCES = [ |
||||
"https://api.ipify.org/", |
||||
"https://icanhazip.com/", |
||||
"https://ifconfig.me/", |
||||
] |
||||
|
||||
# For some reason the default urllib User-Agent is blocked |
||||
FAKE_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.169 Safari/537.36" |
||||
|
||||
|
||||
def setup_logger() -> None: |
||||
form = logging.Formatter("%(asctime)s | %(levelname)s | %(message)s") |
||||
|
||||
handler = logging.StreamHandler(sys.stdout) |
||||
handler.setLevel(logging.INFO) |
||||
handler.setFormatter(form) |
||||
|
||||
logger.setLevel(logging.INFO) |
||||
logger.addHandler(handler) |
||||
|
||||
|
||||
def get_ip_address() -> str: |
||||
url = random.choice(IP_SOURCES) |
||||
res = request.urlopen(url) |
||||
return res.read().decode("utf8").strip() |
||||
|
||||
|
||||
def sign_message(message: bytes, key: bytes) -> str: |
||||
message_hmac = hmac.new(key, message, digestmod="sha256") |
||||
return message_hmac.hexdigest() |
||||
|
||||
|
||||
def update_dns_record(url: str, key: str): |
||||
ip_addr = get_ip_address() |
||||
timestamp = int(datetime.now().timestamp()) |
||||
payload = json.dumps({"addr": ip_addr, "timestamp": timestamp}).encode("utf8") |
||||
signature = sign_message(payload, key.encode("utf8")) |
||||
|
||||
req = request.Request(f"https://{url}") |
||||
req.add_header("Content-Type", "application/json; charset=utf-8") |
||||
req.add_header("User-Agent", FAKE_USER_AGENT) |
||||
req.add_header("Authorization", signature) |
||||
req.add_header("Content-Length", len(payload)) |
||||
request.urlopen(req, payload) |
||||
logger.info("DNS Record updated successfully") |
||||
|
||||
|
||||
if __name__ == "__main__": |
||||
setup_logger() |
||||
key = os.environ.get("SHARED_KEY") |
||||
url = os.environ.get("WORKER_URL") |
||||
if key and url: |
||||
try: |
||||
update_dns_record(url, key) |
||||
except (error.URLError, error.HTTPError) as err: |
||||
logger.exception("Failed to update DNS record") |
||||
else: |
||||
logger.error("Cannot find configs. Aborting DNS update") |
@ -0,0 +1,137 @@ |
||||
/** |
||||
* This handled a request to update a given DNS record. |
||||
* The request should have the following format: |
||||
* |
||||
* { "addr": "<ipv4_addr>", "timestamp": <unix_timestamp> } |
||||
* |
||||
* The request must be made by the machine that the record will be pointed to |
||||
* and contain the HMAC (of the request body) in the "Authorization" header |
||||
*/ |
||||
addEventListener("fetch", (event) => { |
||||
event.respondWith(handleRequest(event.request)); |
||||
}); |
||||
|
||||
/** |
||||
* Handles the request and validates if changes should be made or not |
||||
* @param {Request} request |
||||
*/ |
||||
async function handleRequest(request) { |
||||
if (request.method === "POST") { |
||||
let valid_request = await is_valid(request); |
||||
if (valid_request) { |
||||
const addr = request.headers.get("cf-connecting-ip"); |
||||
await updateRecord(addr); |
||||
return new Response("Não há gente como a gente", { status: 200 }); |
||||
} |
||||
} |
||||
return new Response("Por cima", { status: 401 }); |
||||
} |
||||
|
||||
/** |
||||
* Checks if it is a valid and authentic request |
||||
* @param {Request} request |
||||
*/ |
||||
async function is_valid(request) { |
||||
const window = 300; // 5 minutes in seconds
|
||||
const rawBody = await request.text(); |
||||
let bodyContent = {}; |
||||
try { |
||||
bodyContent = JSON.parse(rawBody); |
||||
} catch (e) { |
||||
return false; |
||||
} |
||||
|
||||
const sourceAddr = request.headers.get("cf-connecting-ip"); |
||||
const signature = request.headers.get("authorization"); |
||||
if (!signature || !bodyContent.addr || sourceAddr != bodyContent.addr) { |
||||
return false; |
||||
} |
||||
|
||||
const valid_hmac = await verifyHMAC(signature, rawBody); |
||||
if (!valid_hmac) { |
||||
return false; |
||||
} |
||||
|
||||
const now = Math.floor(Date.now() / 1000); |
||||
if (now - bodyContent.timestamp > window) { |
||||
return false; |
||||
} |
||||
return true; |
||||
} |
||||
|
||||
/** |
||||
* Verifies the provided HMAC matches the message |
||||
* @param {String} signature |
||||
* @param {String} message |
||||
*/ |
||||
async function verifyHMAC(signature, message) { |
||||
let encoder = new TextEncoder(); |
||||
let key = await crypto.subtle.importKey( |
||||
"raw", |
||||
encoder.encode(SHARED_KEY), |
||||
{ name: "HMAC", hash: { name: "SHA-256" } }, |
||||
false, |
||||
["verify"] |
||||
); |
||||
|
||||
result = await crypto.subtle.verify( |
||||
"HMAC", |
||||
key, |
||||
hexToArrayBuffer(signature), |
||||
encoder.encode(message) |
||||
); |
||||
return result; |
||||
} |
||||
|
||||
/** |
||||
* Updates the DNS record with the provided IP |
||||
* @param {String} addr |
||||
*/ |
||||
async function updateRecord(addr) { |
||||
const base = "https://api.cloudflare.com/client/v4/zones"; |
||||
const init = { headers: { Authorization: `Bearer ${CF_API_TOKEN}` } }; |
||||
let record; |
||||
|
||||
let record_res = await fetch( |
||||
`${base}/${ZONE}/dns_records?name=${DNS_RECORD}`, |
||||
init |
||||
); |
||||
if (record_res.ok) { |
||||
record = (await record_res.json()).result[0]; |
||||
} else { |
||||
console.log("Get record failed"); |
||||
return; |
||||
} |
||||
|
||||
if (record.content != addr) { |
||||
init.method = "PATCH"; |
||||
init.body = JSON.stringify({ content: addr }); |
||||
await fetch(`${base}/${ZONE}/dns_records/${record.id}`, init); |
||||
console.log("Updated record"); |
||||
} else { |
||||
console.log("Record content is the same, skipping update"); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Transforms an HEX string into an ArrayBuffer |
||||
* Original work of: https://github.com/LinusU/hex-to-array-buffer
|
||||
* @param {String} hex |
||||
*/ |
||||
function hexToArrayBuffer(hex) { |
||||
if (typeof hex !== "string") { |
||||
throw new TypeError("Expected input to be a string"); |
||||
} |
||||
|
||||
if (hex.length % 2 !== 0) { |
||||
throw new RangeError("Expected string to be an even number of characters"); |
||||
} |
||||
|
||||
var view = new Uint8Array(hex.length / 2); |
||||
|
||||
for (var i = 0; i < hex.length; i += 2) { |
||||
view[i / 2] = parseInt(hex.substring(i, i + 2), 16); |
||||
} |
||||
|
||||
return view.buffer; |
||||
} |
Loading…
Reference in new issue