client-show.ts
Raw
1import { fileTypeFromBuffer } from "file-type";
2import hljs from "highlight.js";
3import { decrypt } from "./crypto";
4import { getMode, humanFileSize, isValidUTF8, setupModeRadios } from "./shared";
5
6async 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
52let encrypted: Uint8Array | undefined;
53let content: Uint8Array;
54let filetype: string;
55let filename: string;
56
57const 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.
65const 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.
71function 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
87async 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 encrypted = new Uint8Array(await response.arrayBuffer());
94 filetype = response.headers.get("filetype") || "none";
95 filename = response.headers.get("filename") || "";
96 }
97 try {
98 content = await decrypt(encrypted, password);
99 } catch (_e) {
100 if (passwordLabel) passwordLabel.textContent = "Incorrect password";
101 return false;
102 }
103 sessionStorage.setItem(PASSWORD_KEY, password);
104 document.getElementById("decrypt-overlay")?.remove();
105 const downloadForm = document.getElementById(
106 "download-form",
107 ) as HTMLFormElement | null;
108 downloadForm?.setAttribute("action", "");
109 downloadForm?.addEventListener("submit", (e) => {
110 e.preventDefault();
111 const blob = new Blob([content as BlobPart]);
112 const url = URL.createObjectURL(blob);
113 const link = document.createElement("a");
114 link.download = filename;
115 link.href = url;
116 link.click();
117 });
118 showContent(content, filetype);
119 return true;
120}
121
122setupModeRadios("decrypt_mode");
123
124// If we already decrypted this file this session, auto-decrypt with the stored
125// password instead of prompting again. Drop a stale password if it no longer works.
126const savedPassword = sessionStorage.getItem(PASSWORD_KEY);
127if (savedPassword) {
128 decryptClientSide(savedPassword, null).then((ok) => {
129 if (!ok) sessionStorage.removeItem(PASSWORD_KEY);
130 });
131}
132
133Object.assign(window, { onPasswordSubmit });
134