-
-
Save GottZ/201bcec70401daf6be552020ec4b0062 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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