client-show.ts
| 1 | import { fileTypeFromBuffer } from "file-type"; |
| 2 | import hljs from "highlight.js"; |
| 3 | import { decrypt } from "./crypto"; |
| 4 | import { getMode, humanFileSize, isValidUTF8, setupModeRadios } from "./shared"; |
| 5 | |
| 6 | async function showContent(content: Uint8Array, filetype: string) { |
| 7 | const filesize = document.getElementById("filesize") as HTMLDivElement | null; |
| 8 | if (filesize) { |
| 9 | filesize.textContent = `size: ${humanFileSize(content.byteLength)}`; |
| 10 | } |
| 11 | |
| 12 | const mediaBox = document.getElementById("mediabox") as HTMLDivElement | null; |
| 13 | if (!mediaBox) { |
| 14 | console.error("Missing mediaBox element."); |
| 15 | return; |
| 16 | } |
| 17 | let preview: HTMLElement = document.createElement("div"); |
| 18 | preview.textContent = "This file can't be previewed"; |
| 19 | if (isValidUTF8(content) && filetype !== "blob") { |
| 20 | preview = document.createElement("pre"); |
| 21 | if (filetype === "none") { |
| 22 | preview.textContent = new TextDecoder().decode(content); |
| 23 | } else { |
| 24 | preview.innerHTML = hljs.highlight( |
| 25 | new TextDecoder("utf-8").decode(content), |
| 26 | { language: filetype }, |
| 27 | ).value; |
| 28 | } |
| 29 | } else { |
| 30 | const filetype = await fileTypeFromBuffer(content); |
| 31 | if (filetype) { |
| 32 | const blob = new Blob([content as BlobPart], { type: filetype.mime }); |
| 33 | const url = URL.createObjectURL(blob); |
| 34 | if (filetype.mime.startsWith("audio/")) { |
| 35 | preview = document.createElement("audio"); |
| 36 | preview.setAttribute("controls", ""); |
| 37 | preview.setAttribute("src", url); |
| 38 | } else if (filetype.mime.startsWith("video/")) { |
| 39 | preview = document.createElement("video"); |
| 40 | preview.setAttribute("controls", ""); |
| 41 | preview.setAttribute("src", url); |
| 42 | } else if (filetype.mime.startsWith("image/")) { |
| 43 | preview = document.createElement("img"); |
| 44 | preview.setAttribute("src", url); |
| 45 | } |
| 46 | } |
| 47 | } |
| 48 | mediaBox.innerHTML = ""; |
| 49 | mediaBox.appendChild(preview); |
| 50 | } |
| 51 | |
| 52 | let encrypted: Uint8Array | undefined; |
| 53 | let content: Uint8Array; |
| 54 | let filetype: string; |
| 55 | let filename: string; |
| 56 | |
| 57 | const uuid = window.location.pathname |
| 58 | .split("/") |
| 59 | .filter((x) => x !== "") |
| 60 | .reverse()[0]; |
| 61 | |
| 62 | // Remembers the client-side password for this file for the rest of the browser |
| 63 | // session, mirroring the server-side password cookie so the prompt only appears |
| 64 | // once per session. |
| 65 | const PASSWORD_KEY = `zbin-pw-${uuid}`; |
| 66 | |
| 67 | // Called from the overlay form's onsubmit. In server mode we let the form POST |
| 68 | // the password to /set-cookie (native submit). In client mode we intercept, |
| 69 | // fetch the still-encrypted bytes, and decrypt locally so the password never |
| 70 | // leaves the browser. |
| 71 | function onPasswordSubmit(): boolean { |
| 72 | if (getMode("decrypt_mode") === "server") { |
| 73 | return true; |
| 74 | } |
| 75 | const passwordInput = document.getElementById( |
| 76 | "password", |
| 77 | ) as HTMLInputElement | null; |
| 78 | const passwordLabel = document.getElementById( |
| 79 | "password-label", |
| 80 | ) as HTMLLabelElement | null; |
| 81 | if (passwordInput) { |
| 82 | void decryptClientSide(passwordInput.value, passwordLabel); |
| 83 | } |
| 84 | return false; |
| 85 | } |
| 86 | |
| 87 | async function decryptClientSide( |
| 88 | password: string, |
| 89 | passwordLabel: HTMLLabelElement | null, |
| 90 | ): Promise<boolean> { |
| 91 | if (!encrypted) { |
| 92 | const response = await fetch(`/raw/${uuid}?ignore_password=true`); |
| 93 | if (!response.ok) { |
| 94 | if (passwordLabel) |
| 95 | passwordLabel.textContent = "Couldn't fetch the file, try reloading"; |
| 96 | return false; |
| 97 | } |
| 98 | encrypted = new Uint8Array(await response.arrayBuffer()); |
| 99 | filetype = response.headers.get("filetype") || "none"; |
| 100 | filename = response.headers.get("filename") || ""; |
| 101 | } |
| 102 | try { |
| 103 | content = await decrypt(encrypted, password); |
| 104 | } catch (_e) { |
| 105 | if (passwordLabel) passwordLabel.textContent = "Incorrect password"; |
| 106 | return false; |
| 107 | } |
| 108 | sessionStorage.setItem(PASSWORD_KEY, password); |
| 109 | document.getElementById("decrypt-overlay")?.remove(); |
| 110 | const downloadForm = document.getElementById( |
| 111 | "download-form", |
| 112 | ) as HTMLFormElement | null; |
| 113 | downloadForm?.setAttribute("action", ""); |
| 114 | downloadForm?.addEventListener("submit", (e) => { |
| 115 | e.preventDefault(); |
| 116 | const blob = new Blob([content as BlobPart]); |
| 117 | const url = URL.createObjectURL(blob); |
| 118 | const link = document.createElement("a"); |
| 119 | link.download = filename; |
| 120 | link.href = url; |
| 121 | link.click(); |
| 122 | }); |
| 123 | showContent(content, filetype); |
| 124 | return true; |
| 125 | } |
| 126 | |
| 127 | setupModeRadios("decrypt_mode"); |
| 128 | |
| 129 | // If we already decrypted this file this session, auto-decrypt with the stored |
| 130 | // password instead of prompting again. Drop a stale password if it no longer works. |
| 131 | const savedPassword = sessionStorage.getItem(PASSWORD_KEY); |
| 132 | if (savedPassword) { |
| 133 | decryptClientSide(savedPassword, null).then((ok) => { |
| 134 | if (!ok) sessionStorage.removeItem(PASSWORD_KEY); |
| 135 | }); |
| 136 | } |
| 137 | |
| 138 | Object.assign(window, { onPasswordSubmit }); |
| 139 |