Skip to content

Instantly share code, notes, and snippets.

@GottZ

GottZ/worker.js Secret

Created August 1, 2023 08:02
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save GottZ/201bcec70401daf6be552020ec4b0062 to your computer and use it in GitHub Desktop.
Save GottZ/201bcec70401daf6be552020ec4b0062 to your computer and use it in GitHub Desktop.
const APITOKEN = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
const users = {
"blauser": {
"suchpassword": {
domain: "foo.example.com",
proxied: false,
onlyv4: true,
},
},
"blauser2": {
"suchpassword2": {
domain: "foobar.example.com",
proxied: true,
onlyv4: false,
},
},
};
/**
* Parse HTTP Basic Authorization value.
* @param {Request} request
* @returns {{ isBasic: bool, user: string, pass: string }}
*/
function basicAuthentication(request) {
const Authorization = request.headers.get('Authorization');
const [scheme, encoded] = Authorization.split(' ');
// The Authorization header must start with Basic, followed by a space.
if (!encoded || scheme !== 'Basic') {
return {
isBasic: false,
user: null,
password: null,
};
}
// Decodes the base64 value and performs unicode normalization.
// @see https://datatracker.ietf.org/doc/html/rfc7613#section-3.3.2 (and #section-4.2.2)
// @see https://dev.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String/normalize
const buffer = Uint8Array.from(atob(encoded), character => character.charCodeAt(0));
const decoded = new TextDecoder().decode(buffer).normalize();
// The username & password are split by the first colon.
//=> example: "username:password"
const index = decoded.indexOf(':');
// The user & password are split by the first colon and MUST NOT contain control characters.
// @see https://tools.ietf.org/html/rfc5234#appendix-B.1 (=> "CTL = %x00-1F / %x7F")
if (index === -1 || /[\0-\x1F\x7F]/.test(decoded)) {
return {
isBasic: false,
user: null,
password: null,
};
}
return {
isBasic: true,
user: decoded.substring(0, index),
password: decoded.substring(index + 1),
};
}
const IP = (()=>{
// extracted and modified from https://github.com/sindresorhus/ip-regex
const word = '[a-fA-F\\d:]';
const b = options => options && options.includeBoundaries ?
`(?:(?<=\\s|^)(?=${word})|(?<=${word})(?=\\s|$))` :
'';
const v4 = '(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)(?:\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)){3}';
const v6seg = '[a-fA-F\\d]{1,4}';
const v6 = `
(
(?:${v6seg}:){7}(?:${v6seg}|:)| // 1:2:3:4:5:6:7:: 1:2:3:4:5:6:7:8
(?:${v6seg}:){6}(?:${v4}|:${v6seg}|:)| // 1:2:3:4:5:6:: 1:2:3:4:5:6::8 1:2:3:4:5:6::8 1:2:3:4:5:6::1.2.3.4
(?:${v6seg}:){5}(?::${v4}|(:${v6seg}){1,2}|:)| // 1:2:3:4:5:: 1:2:3:4:5::7:8 1:2:3:4:5::8 1:2:3:4:5::7:1.2.3.4
(?:${v6seg}:){4}(?:(:${v6seg}){0,1}:${v4}|(:${v6seg}){1,3}|:)| // 1:2:3:4:: 1:2:3:4::6:7:8 1:2:3:4::8 1:2:3:4::6:7:1.2.3.4
(?:${v6seg}:){3}(?:(:${v6seg}){0,2}:${v4}|(:${v6seg}){1,4}|:)| // 1:2:3:: 1:2:3::5:6:7:8 1:2:3::8 1:2:3::5:6:7:1.2.3.4
(?:${v6seg}:){2}(?:(:${v6seg}){0,3}:${v4}|(:${v6seg}){1,5}|:)| // 1:2:: 1:2::4:5:6:7:8 1:2::8 1:2::4:5:6:7:1.2.3.4
(?:${v6seg}:){1}(?:(:${v6seg}){0,4}:${v4}|(:${v6seg}){1,6}|:)| // 1:: 1::3:4:5:6:7:8 1::8 1::3:4:5:6:7:1.2.3.4
(?::((?::${v6seg}){0,5}:${v4}|(?::${v6seg}){1,7}|:)) // ::2:3:4:5:6:7:8 ::2:3:4:5:6:7:8 ::8 ::1.2.3.4
)(%[0-9a-zA-Z]{1,})? // %eth0 %1
`.replace(/\s*\/\/.*$/gm, '').replace(/^\s*/gm, '').replace(/\n/g, '').trim();
const ip = options => options && options.exact ?
new RegExp(`(?:^${v4}$)|(?:^${v6}$)`) :
new RegExp(`(?:${b(options)}${v4}${b(options)})|(?:${b(options)}${v6}${b(options)})`, 'g');
ip.v4 = options => options && options.exact ? new RegExp(`^${v4}$`) : new RegExp(`${b(options)}${v4}${b(options)}`, 'g');
ip.v6 = options => options && options.exact ? new RegExp(`^${v6}$`) : new RegExp(`${b(options)}${v6}${b(options)}`, 'g');
ip.isv4 = ip.v4({exact:true});
ip.isv6 = ip.v6({exact:true});
ip.type = (text) => ip.isv4.test(text) ? 4 : ip.isv6.test(text) ? 6 : undefined;
return ip;
})();
addEventListener('fetch', event => {
event.respondWith(handleRequest(event.request))
})
async function handleSpeedPort(params, request) {
const {isBasic, user, password} = basicAuthentication(request);
const myip = params.myip?.shift();
const hostname = params.hostname?.shift();
if (
!isBasic || [myip, hostname, user, password].some(data => typeof data !== "string")
) {
return new Response("missing or malformed parameters", {status: 400});
}
return handleUpdate({
user: [user],
password: [password],
ip: myip.split(","),
domain: [hostname],
});
}
async function handleUpdate(params) {
const user = params.user?.shift();
const password = params.password?.shift();
const domain = params.domain?.shift();
const ips = params.ip ? params.ip : [null];
if (params.dbg) return new Response(JSON.stringify({a:[...ips, user, password, domain]}));
if (
[...ips, user, password, domain]
.some(data => typeof data !== "string")
) {
return new Response("missing or malformed parameters", {status: 400});
}
const authenticated = user in users && password in users[user];
const config = authenticated ? users[user][password] : undefined;
const debug = user === "debug" && password === "debug";
if (!debug && !authenticated) {
return new Response("unauthorized", {status: 401});
}
if (!debug && config.domain !== domain) {
return new Response("you cannot change this domain name", {status: 403});
}
const v4 = [];
const v6 = [];
const onlyv4 = "onlyv4" in config && config.onlyv4;
ips.filter((value,index,self) => self.indexOf(value) === index).forEach(ip => {
switch(IP.type(ip)) {
case 4: {
v4.push(ip);
break;
}
case 6: {
if(!onlyv4) v6.push(ip);
break;
}
}
});
if (v4.length === 0 && v6.length === 0) {
return new Response("need at least one valid ip", {status: 400});
}
if (debug) {
return new Response(JSON.stringify({authenticated,debug,domain,v4,v6,ips}), {
headers: { 'content-type': 'text/plain' },
});
}
// obtain correct cloudflare zone
// figure out actual domain name:
const zoneName = domain.split(".").slice(-2).join(".");
const zoneId = await fetch(`https://api.cloudflare.com/client/v4/zones?name=${zoneName}`, {
headers: {
"Authorization": `Bearer ${APITOKEN}`,
},
}).then(res => res.json()).then(res => res.result[0].id);
const records = await fetch(`https://api.cloudflare.com/client/v4/zones/${zoneId}/dns_records?name=${domain}`, {
headers: {
"Authorization": `Bearer ${APITOKEN}`,
},
}).then(res => res.json()).then(res => res.result);
const pending = [];
const todo = [];
records.filter(record => /^A(?:AAA)?$/.test(record.type)).forEach(record => {
const list = record.type === "A" ? v4 : v6;
const offset = list.indexOf(record.content);
if (offset >= 0) {
list.splice(offset, 1)[0];
if (record.proxied !== config.proxied) {
todo.push({
type: record.type,
content: record.content,
record,
});
}
} else {
pending.push(record);
}
});
[v4, v6].forEach((list, typeIndex) => {
const type = typeIndex === 0 ? "A" : "AAAA";
list.forEach(content => {
const task = {
type,
content,
record: pending.shift(),
};
todo.push(task);
});
});
if (todo.length === 0 && pending.length === 0) {
return new Response("nochg");
}
const requests = [];
// delete excess
pending.splice(0).forEach(record => {
requests.push(fetch(`https://api.cloudflare.com/client/v4/zones/${zoneId}/dns_records/${record.id}`, {
method: "DELETE",
headers: {
"Authorization": `Bearer ${APITOKEN}`,
},
}));
});
// change and create records
todo.forEach(task => {
const doUpdate = typeof task.record !== "undefined";
const record = task.record;
const body = {};
if (doUpdate) {
if (record.type !== task.type) body.type = task.type;
if (record.content !== task.content) body.content = task.content;
if (record.proxied !== config.proxied) body.proxied = config.proxied;
if (!config.proxied && record.ttl != 60) body.ttl = 60;
} else {
body.ttl = config.proxied ? 1 : 60;
body.proxied = config.proxied;
body.type = task.type;
body.name = domain;
body.content = task.content;
}
requests.push(fetch(`https://api.cloudflare.com/client/v4/zones/${zoneId}/dns_records${
doUpdate ? `/${record.id}` : ""
}`, {
method: doUpdate ? "PATCH" : "POST",
body: JSON.stringify(body),
headers: {
"Authorization": `Bearer ${APITOKEN}`,
"Content-Type": "application/json",
},
}));
});
const data = await Promise.all(requests.map(req => req.then(res => res.json()))).then(requests => JSON.stringify(requests));
return new Response("good");
/*return new Response(JSON.stringify({
todo,
deletable: pending,
data,
}));*/
};
/**
* Respond with hello worker text
* @param {Request} request
*/
async function handleRequest(request) {
if (request.method !== "GET") {
return new Response("abuse", {status: 403});
}
const url = new URL(request.url);
const params = {};
new Set(url.searchParams.keys()).forEach(key => params[key] = url.searchParams.getAll(key));
switch(url.pathname) {
case "/update": {
return await handleUpdate(params);
}
case "/nic/update": {
return await handleSpeedPort(params, request);
}
case "/fritz": {
return new Response("http://ddns.gottz.de/update?user=<username>&password=<pass>&domain=<domain>&ip=<ipaddr>&ip=<ip6addr>");
}
case "/robots.txt": {
return new Response("User-agent: *\nDisallow: /\n");
}
default: {
return new Response("abuse", {status: 404});
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment