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 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
127setupModeRadios("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.
131const savedPassword = sessionStorage.getItem(PASSWORD_KEY);
132if (savedPassword) {
133 decryptClientSide(savedPassword, null).then((ok) => {
134 if (!ok) sessionStorage.removeItem(PASSWORD_KEY);
135 });
136}
137
138Object.assign(window, { onPasswordSubmit });
139