Files
4ev.link/functions/[[path]].js

151 lines
4.3 KiB
JavaScript

/**
* Configuration and Constants
*/
const CHARSET = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
const BASE_SLUG_LENGTH = 4;
const CORS_HEADERS = {
'Access-Control-Allow-Origin': '*',
};
const OPTIONS_HEADERS = {
...CORS_HEADERS,
'Access-Control-Allow-Methods': 'POST',
'Access-Control-Allow-Headers': 'Content-Type',
};
const JSON_HEADERS = {
...CORS_HEADERS,
'Content-Type': 'application/json',
};
/**
* Generates a random string of a given length from the character set.
* @param {number} length The desired length of the slug.
* @returns {string} The generated slug.
*/
const generateSlug = (length) => {
return [...Array(length)]
.map(() => CHARSET[Math.floor(Math.random() * CHARSET.length)])
.join('');
};
/**
* Tries to parse a string into a valid URL, prepending 'https://' if necessary.
* @param {string} urlString The string to normalize.
* @returns {string|undefined} The normalized URL as a string, or undefined if invalid.
*/
const normalizeUrl = (urlString) => {
try {
return new URL(urlString).href;
} catch {
try {
return new URL('https://' + urlString).href;
} catch {
return undefined;
}
}
};
/**
* Verifies the user's reCAPTCHA token with Google's API.
* @param {string} token The reCAPTCHA token from the client.
* @param {string} secretKey The reCAPTCHA secret key.
* @returns {Promise<boolean>} A promise that resolves to true if the token is valid.
*/
async function verifyRecaptcha(token, secretKey) {
if (!token || !secretKey) {
return false;
}
const response = await fetch('https://www.google.com/recaptcha/api/siteverify', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: `secret=${secretKey}&response=${token}`,
});
const data = await response.json();
return data.success;
}
/**
* Handles the creation of a new shortened link.
* @param {Request} request The incoming request object.
* @param {object} env The environment bindings.
* @returns {Promise<Response>} A promise that resolves to a Response object.
*/
async function handleCreateRequest(request, env) {
try {
const { url: targetUrl, token } = await request.json();
if (!await verifyRecaptcha(token, env.RECAPCHA_KEY)) {
return new Response('CAPTCHA verification failed.', { status: 403, headers: CORS_HEADERS });
}
const normalizedTarget = normalizeUrl(targetUrl);
if (!normalizedTarget) {
return new Response('Invalid URL provided.', { status: 400, headers: CORS_HEADERS });
}
let slugLength = BASE_SLUG_LENGTH;
let slug;
// Keep generating slugs until a unique one is found.
// If a collision occurs, increase the slug length and try again.
while (true) {
slug = generateSlug(slugLength);
if (!(await env.EV.get(slug))) {
break; // Found a unique slug.
}
slugLength++; // Collision detected, increase length for the next attempt.
}
await env.EV.put(slug, normalizedTarget);
const { origin } = new URL(request.url);
const responsePayload = {
slug,
target: normalizedTarget,
shortUrl: `${origin}/${slug}`,
};
return new Response(JSON.stringify(responsePayload), { headers: JSON_HEADERS });
} catch {
return new Response('Invalid request payload.', { status: 400, headers: CORS_HEADERS });
}
}
/**
* Handles redirecting a short URL to its target URL.
* @param {string} pathname The pathname from the request URL.
* @param {object} env The environment bindings.
* @returns {Promise<Response|null>} A redirect Response or null if the slug is not found.
*/
async function handleRedirectRequest(pathname, env) {
const slug = pathname.slice(1);
if (slug) {
const targetUrl = await env.EV.get(slug);
if (targetUrl) {
return Response.redirect(targetUrl, 302);
}
}
return null;
}
/**
* Main request handler for the Cloudflare Worker.
*/
export async function onRequest({ request, env, next }) {
if (request.method === 'OPTIONS') {
return new Response(null, { headers: OPTIONS_HEADERS });
}
const url = new URL(request.url);
if (url.pathname === '/api/create' && request.method === 'POST') {
return handleCreateRequest(request, env);
}
const redirectResponse = await handleRedirectRequest(url.pathname, env);
if (redirectResponse) {
return redirectResponse;
}
return next();
}