Skip to content

Commit

Permalink
GPT-4 refactor
Browse files Browse the repository at this point in the history
  • Loading branch information
willswire committed Apr 23, 2023
1 parent aa9ad77 commit 5aa095f
Showing 1 changed file with 110 additions and 176 deletions.
286 changes: 110 additions & 176 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,123 +1,86 @@
/**
* Receives a HTTP request and replies with a response.
* @param {Request} request
* @returns {Promise<Response>}
*/
async function handleRequest(request) {
const { protocol, pathname } = new URL(request.url);

// Require HTTPS (TLS) connection to be secure.
if (
"https:" !== protocol ||
"https" !== request.headers.get("x-forwarded-proto")
) {
throw new BadRequestException("Please use a HTTPS connection.");
class BadRequestException extends Error {
constructor(reason) {
super(reason);
this.status = 400;
this.statusText = "Bad Request";
}
}

switch (pathname) {

case "/nic/update":
case "/update":
if (request.headers.has("Authorization")) {
const { username, password } = basicAuthentication(request);

// Throws exception when query parameters aren't formatted correctly
const url = new URL(request.url);
verifyParameters(url);

// Only returns this response when no exception is thrown.
const response = await informAPI(url, username, password);
return response;
}

throw new BadRequestException("Please provide valid credentials.");

case "/favicon.ico":
case "/robots.txt":
return new Response(null, { status: 204 });
class CloudflareApiException extends Error {
constructor(reason) {
super(reason);
this.status = 500;
this.statusText = "Internal Server Error";
}

return new Response("Not Found.", { status: 404 });
}

/**
* Pass the request info to the Cloudflare API Handler
* @param {URL} url
* @param {String} name
* @param {String} token
* @returns {Promise<Response>}
*/
async function informAPI(url, name, token) {
// Parse Url
const hostnames = url.searchParams.get("hostname").split(",");
// Get the IP address. This can accept two query parameters, this will
// use the "ip" query parameter if it is set, otherwise falling back to "myip".
const ip = url.searchParams.get("ip") || url.searchParams.get("myip");

// Initialize API Handler
const cloudflare = new Cloudflare({
token: token,
});
class Cloudflare {
constructor(options) {
this.cloudflare_url = "https://api.cloudflare.com/client/v4";
this.token = options.token;
}

const zone = await cloudflare.findZone(name);
for (const hostname of hostnames) {
const record = await cloudflare.findRecord(zone, hostname);
const result = await cloudflare.updateRecord(record, ip);
async findZone(name) {
const response = await this._fetchWithToken(`zones?name=${name}`);
const body = await response.json();
if (!body.success || body.result.length === 0) {
throw new CloudflareApiException(`Failed to find zone '${name}'`);
}
return body.result[0];
}

// Only returns this response when no exception is thrown.
return new Response(`good`, {
status: 200,
headers: {
"Content-Type": "text/plain;charset=UTF-8",
"Cache-Control": "no-store"
},
});
}
async findRecord(zone, name) {
const response = await this._fetchWithToken(`zones/${zone.id}/dns_records?name=${name}`);
const body = await response.json();
if (!body.success || body.result.length === 0) {
throw new CloudflareApiException(`Failed to find dns record '${name}'`);
}
return body.result[0];
}

/**
* Throws exception on verification failure.
* @param {string} url
* @throws {UnauthorizedException}
*/
function verifyParameters(url) {
if (!url.searchParams) {
throw new BadRequestException("You must include proper query parameters");
async updateRecord(record, value) {
record.content = value;
const response = await this._fetchWithToken(
`zones/${record.zone_id}/dns_records/${record.id}`,
{
method: "PUT",
body: JSON.stringify(record),
}
);
const body = await response.json();
if (!body.success) {
throw new CloudflareApiException("Failed to update dns record");
}
return body.result[0];
}

if (!url.searchParams.get("hostname")) {
throw new BadRequestException("You must specify a hostname");
async _fetchWithToken(endpoint, options = {}) {
const url = `${this.cloudflare_url}/${endpoint}`;
options.headers = {
...options.headers,
"Content-Type": "application/json",
Authorization: `Bearer ${this.token}`,
};
return fetch(url, options);
}
}

if (!(url.searchParams.get("ip") || url.searchParams.get("myip"))) {
throw new BadRequestException("You must specify an ip address");
function requireHttps(request) {
const { protocol } = new URL(request.url);
const forwardedProtocol = request.headers.get("x-forwarded-proto");

if (protocol !== "https:" || forwardedProtocol !== "https") {
throw new BadRequestException("Please use a HTTPS connection.");
}
}

/**
* Parse HTTP Basic Authorization value.
* @param {Request} request
* @throws {BadRequestException}
* @returns {{ user: string, pass: string }}
*/
function basicAuthentication(request) {
function parseBasicAuth(request) {
const Authorization = request.headers.get("Authorization");

const [scheme, encoded] = Authorization.split(" ");

// Decodes the base64 value and performs unicode normalization.
// @see https://dev.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String/normalize
const buffer = Uint8Array.from(atob(encoded), (character) =>
character.charCodeAt(0)
);
const buffer = Uint8Array.from(atob(encoded), (c) => c.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)) {
throw new BadRequestException("Invalid authorization value.");
}
Expand All @@ -128,92 +91,65 @@ function basicAuthentication(request) {
};
}

class UnauthorizedException {
constructor(reason) {
this.status = 401;
this.statusText = "Unauthorized";
this.reason = reason;
async function handleRequest(request) {
requireHttps(request);
const { pathname } = new URL(request.url);

if (pathname === "/favicon.ico" || pathname === "/robots.txt") {
return new Response(null, { status: 204 });
}
}

class BadRequestException {
constructor(reason) {
this.status = 400;
this.statusText = "Bad Request";
this.reason = reason;
if (pathname !== "/nic/update" && pathname !== "/update") {
return new Response("Not Found.", { status: 404 });
}
}

class CloudflareApiException {
constructor(reason) {
this.status = 500;
this.statusText = "Internal Server Error";
this.reason = reason;
if (!request.headers.has("Authorization")) {
throw new BadRequestException("Please provide valid credentials.");
}

const { username, password } = parseBasicAuth(request);
const url = new URL(request.url);
verifyParameters(url);

const response = await informAPI(url, username, password);
return response;
}

class Cloudflare {
constructor(options) {
this.cloudflare_url = "https://api.cloudflare.com/client/v4";
function verifyParameters(url) {
const { searchParams } = url;

if (options.token) {
this.token = options.token;
}
if (!searchParams) {
throw new BadRequestException("You must include proper query parameters");
}

this.findZone = async (name) => {
var response = await fetch(
`https://api.cloudflare.com/client/v4/zones?name=${name}`,
{
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${this.token}`,
},
}
);
var body = await response.json();
if (body.success !== true || body.result.length === 0) {
throw new CloudflareApiException("Failed to find zone '" + name + "'");
}
return body.result[0];
};
if (!searchParams.get("hostname")) {
throw new BadRequestException("You must specify a hostname");
}

this.findRecord = async (zone, name) => {
var response = await fetch(
`https://api.cloudflare.com/client/v4/zones/${zone.id}/dns_records?name=${name}`,
{
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${this.token}`,
},
}
);
var body = await response.json();
if (body.success !== true || body.result.length === 0) {
throw new CloudflareApiException("Failed to find dns record '" + name + "'");
}
return body.result[0];
};
if (!(searchParams.get("ip") || searchParams.get("myip"))) {
throw new BadRequestException("You must specify an ip address");
}
}

this.updateRecord = async (record, value) => {
record.content = value;
var response = await fetch(
`https://api.cloudflare.com/client/v4/zones/${record.zone_id}/dns_records/${record.id}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${this.token}`,
},
body: JSON.stringify(record),
}
);
var body = await response.json();
if (body.success !== true) {
throw new CloudflareApiException("Failed to update dns record");
}
return body.result[0];
};
async function informAPI(url, name, token) {
const hostnames = url.searchParams.get("hostname").split(",");
const ip = url.searchParams.get("ip") || url.searchParams.get("myip");

const cloudflare = new Cloudflare({ token });

const zone = await cloudflare.findZone(name);
for (const hostname of hostnames) {
const record = await cloudflare.findRecord(zone, hostname);
await cloudflare.updateRecord(record, ip);
}

return new Response("good", {
status: 200,
headers: {
"Content-Type": "text/plain;charset=UTF-8",
"Cache-Control": "no-store",
},
});
}

export default {
Expand All @@ -227,12 +163,10 @@ export default {
statusText: err.statusText || null,
headers: {
"Content-Type": "text/plain;charset=UTF-8",
// Disables caching by default.
"Cache-Control": "no-store",
// Returns the "Content-Length" header for HTTP HEAD requests.
"Content-Length": message.length,
},
});
})
});
},
};

0 comments on commit 5aa095f

Please sign in to comment.