-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathautoselect.js
More file actions
141 lines (126 loc) · 6.06 KB
/
Copy pathautoselect.js
File metadata and controls
141 lines (126 loc) · 6.06 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
"use strict";
const downloadAndSelect = document.querySelector("#download-and-select");
const encryptedFileInput = document.querySelector("#encrypted-file");
const encryptedFileInfo = document.querySelector("#file-info");
const pageStatus = document.querySelector("#status");
const tokenPattern = /^[A-Za-z0-9_-]{43}$/;
const filenamePattern = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
const activeKey = "keydrop-active-v1";
downloadAndSelect.addEventListener("click", async () => {
encryptedFileInfo.textContent = "Скачиваю и выбираю файл…";
try {
const response = await fetch(downloadAndSelect.href);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const file = new File(
[await response.blob()],
downloadAndSelect.download,
{ type: "application/octet-stream" },
);
const transfer = new DataTransfer();
transfer.items.add(file);
encryptedFileInput.files = transfer.files;
encryptedFileInput.dispatchEvent(new Event("change", { bubbles: true }));
pageStatus.textContent = "Файл скачан и уже выбран. Проводник Android не нужен.";
pageStatus.dataset.error = "false";
} catch (error) {
pageStatus.textContent = "Скачивание началось, но автоматически выбрать файл не удалось. Выбери его вручную.";
pageStatus.dataset.error = "true";
}
});
const productionReady = startProduction();
async function startProduction() {
const fragment = location.hash.slice(1);
let saved = readActiveDrop();
const incoming = parseDropFragment(fragment);
if (fragment && !incoming) {
clearActiveDrop();
return;
}
if (incoming) {
const replacement = `${location.pathname}${location.search || ""}`;
history.replaceState(null, "", replacement);
const lease = saved?.token === incoming.token && tokenPattern.test(saved.lease) ? saved.lease : randomToken();
saved = { token: incoming.token, lease, filename: incoming.filename };
writeActiveDrop(saved);
}
if (!saved || !tokenPattern.test(saved.token) || !tokenPattern.test(saved.lease)) return;
const filename = filenamePattern.test(saved.filename || "") ? saved.filename : "delivery";
setProductionMode();
encryptedFileInfo.textContent = "Получаю зашифрованный файл…";
try {
const claim = await dropRequest("/api/v1/drop/claim", "POST", saved);
if (!claim.ok) {
if (claim.status === 410) clearActiveDrop();
throw new Error(claim.status === 410 ? "Ссылка истекла" : "Не удалось получить файл");
}
const response = await dropRequest("/api/v1/drop", "GET", saved);
if (!response.ok) {
if (response.status === 410) clearActiveDrop();
throw new Error(response.status === 410 ? "Ссылка истекла" : "Не удалось получить файл");
}
const size = Number(response.headers.get("content-length"));
if (!Number.isSafeInteger(size) || size < 1 || size > 16 * 1024 * 1024) throw new Error("Недопустимый размер файла");
const bytes = await response.arrayBuffer();
if (bytes.byteLength !== size) throw new Error("Файл получен не полностью");
const expected = response.headers.get("x-keydrop-sha256") || "";
if (expected !== await sha256Hex(bytes)) throw new Error("Контрольная сумма файла не совпала");
selectEncryptedFile(new File([bytes], `${filename}.enc`, { type: "application/octet-stream" }));
pageStatus.textContent = "Файл получен и выбран. Введи пароль доставки. Эту ссылку можно открыть повторно до истечения срока.";
pageStatus.dataset.error = "false";
} catch (error) {
pageStatus.textContent = error.message || "Не удалось получить файл";
pageStatus.dataset.error = "true";
}
}
function parseDropFragment(fragment) {
const parts = fragment.split("/");
if (!tokenPattern.test(parts[0]) || parts.length > 2) return null;
if (parts.length === 1) return { token: parts[0], filename: "delivery" };
let filename;
try { filename = decodeURIComponent(parts[1]); } catch { return null; }
if (!filenamePattern.test(filename)) return null;
return { token: parts[0], filename };
}
function setProductionMode() {
document.querySelector("h1").textContent = "Защищённая доставка";
document.querySelector("#intro").textContent = "Зашифрованный файл доступен по этой ссылке до истечения срока и расшифровывается только на твоём устройстве.";
downloadAndSelect.hidden = true;
document.querySelector("#download-help").hidden = true;
document.querySelector("#fill-test-password").hidden = true;
}
function selectEncryptedFile(file) {
const transfer = new DataTransfer();
transfer.items.add(file);
encryptedFileInput.files = transfer.files;
encryptedFileInput.dispatchEvent(new Event("change", { bubbles: true }));
}
function dropRequest(path, method, drop) {
return fetch(path, {
method,
headers: {
Authorization: `Keydrop ${drop.token}`,
"X-Keydrop-Lease": drop.lease,
},
cache: "no-store",
credentials: "omit",
redirect: "error",
referrerPolicy: "no-referrer",
});
}
function readActiveDrop() {
try { return JSON.parse(sessionStorage.getItem(activeKey)); } catch { return null; }
}
function writeActiveDrop(drop) {
try { sessionStorage.setItem(activeKey, JSON.stringify(drop)); } catch {}
}
function clearActiveDrop() {
try { sessionStorage.removeItem(activeKey); } catch {}
}
function randomToken() {
const bytes = crypto.getRandomValues(new Uint8Array(32));
return btoa(String.fromCharCode(...bytes)).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "");
}
async function sha256Hex(bytes) {
const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", bytes));
return Array.from(digest, (byte) => byte.toString(16).padStart(2, "0")).join("");
}