fix: improve JS security and error handling in html.gleam
This commit is contained in:
119
src/html.gleam
119
src/html.gleam
@@ -121,6 +121,11 @@ fn decrypt_js(encrypted_content: String) -> String {
|
|||||||
content: document.getElementById('content-display')
|
content: document.getElementById('content-display')
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (!els.loading || !els.error || !els.content) {
|
||||||
|
console.error('Required DOM elements not found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!hash) {
|
if (!hash) {
|
||||||
els.loading.classList.add('hidden');
|
els.loading.classList.add('hidden');
|
||||||
els.error.classList.remove('hidden');
|
els.error.classList.remove('hidden');
|
||||||
@@ -128,7 +133,9 @@ fn decrypt_js(encrypted_content: String) -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const keyBase64 = hash.replace(/-/g, '+').replace(/_/g, '/');
|
let keyBase64 = hash.replace(/-/g, '+').replace(/_/g, '/');
|
||||||
|
while (keyBase64.length % 4) keyBase64 += '=';
|
||||||
|
|
||||||
const encryptedBytes = Uint8Array.from(atob(encryptedContent), c => c.charCodeAt(0));
|
const encryptedBytes = Uint8Array.from(atob(encryptedContent), c => c.charCodeAt(0));
|
||||||
const keyBytes = Uint8Array.from(atob(keyBase64), c => c.charCodeAt(0));
|
const keyBytes = Uint8Array.from(atob(keyBase64), c => c.charCodeAt(0));
|
||||||
const iv = encryptedBytes.slice(0, 12);
|
const iv = encryptedBytes.slice(0, 12);
|
||||||
@@ -150,70 +157,106 @@ fn decrypt_js(encrypted_content: String) -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn escape_js_string(s: String) -> String {
|
fn escape_js_string(s: String) -> String {
|
||||||
s
|
let escaped =
|
||||||
|> string.replace("\\", "\\\\")
|
s
|
||||||
|> string.replace("'", "\\'")
|
|> string.replace("\\", "\\\\")
|
||||||
|> string.replace("\"", "\\\"")
|
|> string.replace("'", "\\'")
|
||||||
|> string.replace("\n", "\\n")
|
|> string.replace("\"", "\\\"")
|
||||||
|> string.replace("\r", "\\r")
|
|> string.replace("\n", "\\n")
|
||||||
|
|> string.replace("\r", "\\r")
|
||||||
|
|> string.replace("<", "\\u003c")
|
||||||
|
|> string.replace(">", "\\u003e")
|
||||||
|
|> string.replace("&", "\\u0026")
|
||||||
|
|
||||||
|
escaped
|
||||||
}
|
}
|
||||||
|
|
||||||
fn crypto_js() -> String {
|
fn crypto_js() -> String {
|
||||||
"
|
"
|
||||||
async function encryptContent(content) {
|
async function encryptContent(content) {
|
||||||
const encoder = new TextEncoder();
|
try {
|
||||||
const data = encoder.encode(content);
|
const encoder = new TextEncoder();
|
||||||
const key = await crypto.subtle.generateKey({ name: 'AES-GCM', length: 256 }, true, ['encrypt', 'decrypt']);
|
const data = encoder.encode(content);
|
||||||
const iv = crypto.getRandomValues(new Uint8Array(12));
|
const key = await crypto.subtle.generateKey({ name: 'AES-GCM', length: 256 }, true, ['encrypt', 'decrypt']);
|
||||||
const encrypted = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, key, data);
|
const iv = crypto.getRandomValues(new Uint8Array(12));
|
||||||
const keyData = await crypto.subtle.exportKey('raw', key);
|
const encrypted = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, key, data);
|
||||||
return {
|
const keyData = await crypto.subtle.exportKey('raw', key);
|
||||||
encrypted: btoa(String.fromCharCode(...new Uint8Array([...iv, ...new Uint8Array(encrypted)]))),
|
return {
|
||||||
key: btoa(String.fromCharCode(...new Uint8Array(keyData)))
|
encrypted: btoa(String.fromCharCode(...iv, ...new Uint8Array(encrypted))),
|
||||||
};
|
key: btoa(String.fromCharCode(...new Uint8Array(keyData)))
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Encryption failed:', e);
|
||||||
|
throw new Error('Failed to encrypt content');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function base64ToUrlSafe(base64) {
|
function base64ToUrlSafe(base64) {
|
||||||
return base64.replaceAll('+', '-').replaceAll('/', '_').replace(/=+$/, '');
|
return base64.split('+').join('-').split('/').join('_').replace(/=+$/g, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
function showShareUrl(url) {
|
function showShareUrl(url) {
|
||||||
const card = document.querySelector('.card');
|
const card = document.querySelector('.card');
|
||||||
|
if (!card) {
|
||||||
|
console.error('Card element not found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
const div = document.createElement('div');
|
const div = document.createElement('div');
|
||||||
div.className = 'share-url';
|
div.className = 'share-url';
|
||||||
div.innerHTML = '<label>Share this URL</label>';
|
const label = document.createElement('label');
|
||||||
|
label.textContent = 'Share this URL';
|
||||||
|
div.appendChild(label);
|
||||||
const input = document.createElement('input');
|
const input = document.createElement('input');
|
||||||
input.type = 'text';
|
input.type = 'text';
|
||||||
input.readOnly = true;
|
input.readOnly = true;
|
||||||
input.value = url;
|
input.value = url;
|
||||||
input.onclick = () => input.select();
|
input.addEventListener('click', () => input.select());
|
||||||
div.appendChild(input);
|
div.appendChild(input);
|
||||||
card.insertBefore(div, card.firstChild);
|
card.insertBefore(div, card.firstChild);
|
||||||
input.select();
|
input.select();
|
||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById('paste-form').addEventListener('submit', async (e) => {
|
document.getElementById('paste-form')?.addEventListener('submit', async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const content = document.getElementById('content').value;
|
|
||||||
const { encrypted, key } = await encryptContent(content);
|
|
||||||
document.getElementById('encrypted-content').value = encrypted;
|
|
||||||
const csrfToken = document.getElementById('csrf-token').value;
|
|
||||||
|
|
||||||
const res = await fetch('/', {
|
const submitButton = e.target.querySelector('button[type=\"submit\"]');
|
||||||
method: 'POST',
|
const contentInput = document.getElementById('content');
|
||||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
const encryptedInput = document.getElementById('encrypted-content');
|
||||||
body: `encrypted_content=${encodeURIComponent(encrypted)}&csrf_token=${encodeURIComponent(csrfToken)}`
|
const csrfInput = document.getElementById('csrf-token');
|
||||||
});
|
|
||||||
|
|
||||||
const html = await res.text();
|
if (!contentInput || !encryptedInput || !csrfInput) {
|
||||||
const doc = new DOMParser().parseFromString(html, 'text/html');
|
console.error('Required form elements not found');
|
||||||
const pasteId = doc.querySelector('[data-paste-id]')?.getAttribute('data-paste-id');
|
return;
|
||||||
if (!pasteId) return;
|
}
|
||||||
|
|
||||||
const pasteUrl = `${location.origin}/paste/${pasteId}#${base64ToUrlSafe(key)}`;
|
if (submitButton) submitButton.disabled = true;
|
||||||
document.body.replaceChildren(...doc.body.childNodes);
|
|
||||||
showShareUrl(pasteUrl);
|
try {
|
||||||
history.replaceState({}, '', pasteUrl);
|
const content = contentInput.value;
|
||||||
|
const { encrypted, key } = await encryptContent(content);
|
||||||
|
encryptedInput.value = encrypted;
|
||||||
|
const csrfToken = csrfInput.value;
|
||||||
|
|
||||||
|
const res = await fetch('/', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||||
|
body: `encrypted_content=${encodeURIComponent(encrypted)}&csrf_token=${encodeURIComponent(csrfToken)}`
|
||||||
|
});
|
||||||
|
|
||||||
|
const html = await res.text();
|
||||||
|
const doc = new DOMParser().parseFromString(html, 'text/html');
|
||||||
|
const pasteId = doc.querySelector('[data-paste-id]')?.getAttribute('data-paste-id');
|
||||||
|
if (!pasteId) throw new Error('No paste ID returned');
|
||||||
|
|
||||||
|
const pasteUrl = `${location.origin}/paste/${pasteId}#${base64ToUrlSafe(key)}`;
|
||||||
|
document.body.replaceChildren(...doc.body.childNodes);
|
||||||
|
showShareUrl(pasteUrl);
|
||||||
|
history.replaceState({}, '', pasteUrl);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Form submission failed:', err);
|
||||||
|
alert('Failed to create paste. Please try again.');
|
||||||
|
if (submitButton) submitButton.disabled = false;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
"
|
"
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user