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