Dashy Cloud Backup & Restore Worker
May 24, 2021•886 words
wrangler.toml
name = "dashy-worker"
type = "javascript"
workers_dev = true
route = "example.com/*"
zone_id = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
account_id = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
kv_namespaces = [
{ binding = "DASHY_CLOUD_BACKUP", id = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }
]
index.js
/* Access control headers */
const headers = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, PUT, POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type',
'content-type': 'application/json;charset=UTF-8',
}
/* Listen for incoming requests, and call handler */
addEventListener('fetch', event => {
event.respondWith(handleRequest(event.request));
})
/* Depending on request type, call appropriate function */
async function handleRequest(request) {
const { method } = request;
switch (method) {
case 'GET': return getObject(request); // Return specified object
case 'POST': return addNewObject(request); // Add given object, return ID
case 'PUT': return updateObject(request); // Update specified object with given ID
case 'DELETE': return deleteObject(request); // Delete object for a given ID
default: return handleError(`Unexpected HTTP Method, ${method}`); // Terminate with error
}
}
/* Called when there is an error */
async function handleError(msg = 'Unknown Error') {
return new Response(
JSON.stringify({ errorMsg: `Error: ${msg}` }),
{ status: 200, headers }
);
}
/* Util to update the KV cache */
const setCache = (key, data) => DASHY_CLOUD_BACKUP.put(key, data);
/* Util to return a record from the KV cache */
const getCache = key => DASHY_CLOUD_BACKUP.get(key);
/* Generates a psudo-random string, used as identifiers */
const generateNewBackupId = (totalLen = 16) => {
// 1. Generate Random Characters (based on Math.random)
const letters = (len, str) => {
const newStr = () => String.fromCharCode(65 + Math.floor(Math.random() * 26));
return len <= 1 ? str : str + letters(len - 1, str + newStr());
};
// 2. Generate random numbers (based on time in milliseconds)
const numbers = (len) => Date.now().toString().substr(-len);
// 3. Shuffle order, with a multiplier of JavaScript's Math.random
const shuffled = (letters(totalLen) + numbers(totalLen))
.split('').sort(() => 0.5 - Math.random()).join('');
// 4. Concatinate, hyphonate and return
return shuffled.replace(/(....)./g, '$1-').slice(0, 19);
};
/* Returns CF Response object, with stringified JSON param, and response code */
const returnJsonResponse = (data, code) =>
new Response(
JSON.stringify(data),
{ status: code, headers }
);
/* Gets requesting IP address from headers */
const getIP = (req) => req.headers.get('CF-Connecting-IP');
/* Fetches and returns an object for corresponding backup ID */
async function getObject(request) {
const requestParams = (new URL(request.url)).searchParams;
const backupId = requestParams.get('backupId');
const subHash = requestParams.get('subHash');
if (!backupId) return handleError('Missing Backup ID');
const cache = await getCache(backupId);
if (!cache) return handleError(`No data found for '${backupId}'`);
let userData = {};
try {
userData = JSON.parse(cache);
} catch (e) {
return handleError(`Error parsing returned data, '${e}'`);
}
if (userData.subHash !== subHash) return handleError(`Incorrect Credentials for '${backupId}'`);
if (userData.ip && userData.ip !== getIP(request)) {
return handleError(`You do not have permission to access this record from your current IP`);
}
return returnJsonResponse({ msg: 'Data Returned', backupId, userData }, 200);
}
/* Validates and adds a new record */
async function addNewObject(request) {
const contentType = request.headers.get('content-type') || '';
if (!contentType.includes('application/json')) {
return handleError('JSON was expected');
}
const requestParams = await request.json();
if (!requestParams) return handleError('Body content is missing');
const userData = requestParams.userData;
const subHash = requestParams.subHash;
const bindToIP = requestParams.bindToIp || false;
if (!userData) return handleError('Missing Parameter \'userData\'');
const backupId = generateNewBackupId();
if (await getCache(backupId)) return handleError('Conflicting ID generated, please try again');
const ip = bindToIP ? request.headers.get('CF-Connecting-IP') : null;
const record = JSON.stringify({
userData, backupId, subHash, ip,
});
await setCache(backupId, record);
return returnJsonResponse({ msg: 'Record Added', backupId }, 201);
}
/* Verifies and updates an existing record */
async function updateObject(request) {
// Check that a body is present, and in JSON form
const contentType = request.headers.get('content-type') || '';
if (!contentType.includes('application/json')) {
return handleError('JSON was expected');
}
// Get and check request parameters
const requestParams = await request.json();
if (!requestParams) return handleError('Body content is missing');
const userData = requestParams.userData;
const backupId = requestParams.backupId;
const subHash = requestParams.subHash;
if (!userData) return handleError('Missing Parameter \'userData\'');
if (!backupId) return handleError('Missing Parameter \'backupId\'');
// Check credentials with the current record, to ensure it belongs to the user
const currentRecord = await getCache(backupId);
const parsedRecord = JSON.parse(currentRecord);
if (!currentRecord || !parsedRecord.userData) {
return handleError(`No record found for ID '${backupId}'`);
}
if (parsedRecord.subHash && parsedRecord.subHash !== subHash) {
return handleError('Incorrect Password');
}
if (parsedRecord.ip && parsedRecord.ip !== getIP(request)) {
return handleError('Request not allowed from this IP');
}
// Formulate the new record, and update into key store
const newRecord = JSON.stringify({
userData, backupId, subHash: parsedRecord.subHash, ip: parsedRecord.ip,
});
await setCache(backupId, newRecord);
return returnJsonResponse({ msg: 'Record Updated', backupId }, 201);
}
/* Verifies and deletes a specific record */
async function deleteObject(request) {
// Get parameters to find and check the record against
const requestParams = (new URL(request.url)).searchParams;
const backupId = requestParams.get('backupId');
const subHash = requestParams.get('subHash');
if (!backupId) return handleError('Missing Parameter \'backupId\'');
// Check that everything is kosha, and the user has permission
const currentRecord = await getCache(backupId);
if (!currentRecord) {
return handleError(`No record found for ID '${backupId}'`);
}
let parsedRecord = {};
try {
parsedRecord = JSON.parse(currentRecord);
} catch (e) {
return handleError('Record is malformed');
}
if (!parsedRecord) {
return handleError('Record not available');
}
if (parsedRecord.subHash && parsedRecord.subHash !== subHash) {
return handleError('Incorrect Password');
}
if (parsedRecord.ip && parsedRecord.ip !== getIP(request)) {
return handleError('Request not allowed from this IP');
}
// Proceed to the deletion, and return response
await setCache(backupId, null);
return new Response("Data has been deleted successfully", { status: 200, headers });
}