|
/* 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 }); |
|
} |
Client-Side
The following functions are used to process user data, and make the requests to the worker hosted on CloudFlare
Web Component
Then you just need a form or web component to enable the user to initiate, update, restore from or delete a backup.
In Dashy, this Vue component is located in
CloudBackupRestore.vue
, and it's UI looks something like this:For more info, see Cloud Backup and Restore Docs