mirror of
https://github.com/timvisee/send.git
synced 2026-04-18 21:54:11 -04:00
implemented download tokens
This commit is contained in:
+38
-26
@@ -292,20 +292,13 @@ export function uploadWs(
|
||||
|
||||
////////////////////////
|
||||
|
||||
async function downloadS(id, keychain, signal) {
|
||||
const auth = await keychain.authHeader();
|
||||
|
||||
async function _downloadStream(id, dlToken, signal) {
|
||||
const response = await fetch(getApiUrl(`/api/download/${id}`), {
|
||||
signal: signal,
|
||||
method: 'GET',
|
||||
headers: { Authorization: auth }
|
||||
headers: { Authorization: `Bearer ${dlToken}` }
|
||||
});
|
||||
|
||||
const authHeader = response.headers.get('WWW-Authenticate');
|
||||
if (authHeader) {
|
||||
keychain.nonce = parseNonce(authHeader);
|
||||
}
|
||||
|
||||
if (response.status !== 200) {
|
||||
throw new Error(response.status);
|
||||
}
|
||||
@@ -313,13 +306,13 @@ async function downloadS(id, keychain, signal) {
|
||||
return response.body;
|
||||
}
|
||||
|
||||
async function tryDownloadStream(id, keychain, signal, tries = 2) {
|
||||
async function tryDownloadStream(id, dlToken, signal, tries = 2) {
|
||||
try {
|
||||
const result = await downloadS(id, keychain, signal);
|
||||
const result = await _downloadStream(id, dlToken, signal);
|
||||
return result;
|
||||
} catch (e) {
|
||||
if (e.message === '401' && --tries > 0) {
|
||||
return tryDownloadStream(id, keychain, signal, tries);
|
||||
return tryDownloadStream(id, dlToken, signal, tries);
|
||||
}
|
||||
if (e.name === 'AbortError') {
|
||||
throw new Error('0');
|
||||
@@ -328,21 +321,20 @@ async function tryDownloadStream(id, keychain, signal, tries = 2) {
|
||||
}
|
||||
}
|
||||
|
||||
export function downloadStream(id, keychain) {
|
||||
export function downloadStream(id, dlToken) {
|
||||
const controller = new AbortController();
|
||||
function cancel() {
|
||||
controller.abort();
|
||||
}
|
||||
return {
|
||||
cancel,
|
||||
result: tryDownloadStream(id, keychain, controller.signal)
|
||||
result: tryDownloadStream(id, dlToken, controller.signal)
|
||||
};
|
||||
}
|
||||
|
||||
//////////////////
|
||||
|
||||
async function download(id, keychain, onprogress, canceller) {
|
||||
const auth = await keychain.authHeader();
|
||||
async function download(id, dlToken, onprogress, canceller) {
|
||||
const xhr = new XMLHttpRequest();
|
||||
canceller.oncancel = function() {
|
||||
xhr.abort();
|
||||
@@ -350,10 +342,6 @@ async function download(id, keychain, onprogress, canceller) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
xhr.addEventListener('loadend', function() {
|
||||
canceller.oncancel = function() {};
|
||||
const authHeader = xhr.getResponseHeader('WWW-Authenticate');
|
||||
if (authHeader) {
|
||||
keychain.nonce = parseNonce(authHeader);
|
||||
}
|
||||
if (xhr.status !== 200) {
|
||||
return reject(new Error(xhr.status));
|
||||
}
|
||||
@@ -368,26 +356,26 @@ async function download(id, keychain, onprogress, canceller) {
|
||||
}
|
||||
});
|
||||
xhr.open('get', getApiUrl(`/api/download/blob/${id}`));
|
||||
xhr.setRequestHeader('Authorization', auth);
|
||||
xhr.setRequestHeader('Authorization', `Bearer ${dlToken}`);
|
||||
xhr.responseType = 'blob';
|
||||
xhr.send();
|
||||
onprogress(0);
|
||||
});
|
||||
}
|
||||
|
||||
async function tryDownload(id, keychain, onprogress, canceller, tries = 2) {
|
||||
async function tryDownload(id, dlToken, onprogress, canceller, tries = 2) {
|
||||
try {
|
||||
const result = await download(id, keychain, onprogress, canceller);
|
||||
const result = await download(id, dlToken, onprogress, canceller);
|
||||
return result;
|
||||
} catch (e) {
|
||||
if (e.message === '401' && --tries > 0) {
|
||||
return tryDownload(id, keychain, onprogress, canceller, tries);
|
||||
return tryDownload(id, dlToken, onprogress, canceller, tries);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
export function downloadFile(id, keychain, onprogress) {
|
||||
export function downloadFile(id, dlToken, onprogress) {
|
||||
const canceller = {
|
||||
oncancel: function() {} // download() sets this
|
||||
};
|
||||
@@ -396,7 +384,7 @@ export function downloadFile(id, keychain, onprogress) {
|
||||
}
|
||||
return {
|
||||
cancel,
|
||||
result: tryDownload(id, keychain, onprogress, canceller)
|
||||
result: tryDownload(id, dlToken, onprogress, canceller)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -458,3 +446,27 @@ export async function reportLink(id, keychain, reason) {
|
||||
|
||||
throw new Error(result.response.status);
|
||||
}
|
||||
|
||||
export async function getDownloadToken(id, keychain) {
|
||||
const result = await fetchWithAuthAndRetry(
|
||||
getApiUrl(`/api/download/token/${id}`),
|
||||
{
|
||||
method: 'GET'
|
||||
},
|
||||
keychain
|
||||
);
|
||||
|
||||
if (result.ok) {
|
||||
return (await result.response.json()).token;
|
||||
}
|
||||
throw new Error(result.response.status);
|
||||
}
|
||||
|
||||
export async function downloadDone(id, dlToken) {
|
||||
const headers = new Headers({ Authorization: `Bearer ${dlToken}` });
|
||||
const response = await fetch(getApiUrl(`/api/download/done/${id}`), {
|
||||
headers,
|
||||
method: 'POST'
|
||||
});
|
||||
return response.ok;
|
||||
}
|
||||
|
||||
+5
-2
@@ -250,7 +250,8 @@ export default function(state, emitter) {
|
||||
const start = Date.now();
|
||||
try {
|
||||
const dl = state.transfer.download({
|
||||
stream: state.capabilities.streamDownload
|
||||
stream: state.capabilities.streamDownload,
|
||||
storage: state.storage
|
||||
});
|
||||
render();
|
||||
await dl;
|
||||
@@ -269,7 +270,9 @@ export default function(state, emitter) {
|
||||
} else {
|
||||
// eslint-disable-next-line no-console
|
||||
state.transfer = null;
|
||||
const location = err.message === '404' ? '/404' : '/error';
|
||||
const location = ['404', '403'].includes(err.message)
|
||||
? '/404'
|
||||
: '/error';
|
||||
if (location === '/error') {
|
||||
state.sentry.withScope(scope => {
|
||||
scope.setExtra('duration', err.duration);
|
||||
|
||||
+27
-6
@@ -1,7 +1,14 @@
|
||||
import Nanobus from 'nanobus';
|
||||
import Keychain from './keychain';
|
||||
import { delay, bytes, streamToArrayBuffer } from './utils';
|
||||
import { downloadFile, metadata, getApiUrl, reportLink } from './api';
|
||||
import {
|
||||
downloadFile,
|
||||
downloadDone,
|
||||
metadata,
|
||||
getApiUrl,
|
||||
reportLink,
|
||||
getDownloadToken
|
||||
} from './api';
|
||||
import { blobStream } from './streams';
|
||||
import Zip from './zip';
|
||||
|
||||
@@ -13,9 +20,14 @@ export default class FileReceiver extends Nanobus {
|
||||
this.keychain.setPassword(fileInfo.password, fileInfo.url);
|
||||
}
|
||||
this.fileInfo = fileInfo;
|
||||
this.dlToken = null;
|
||||
this.reset();
|
||||
}
|
||||
|
||||
get id() {
|
||||
return this.fileInfo.id;
|
||||
}
|
||||
|
||||
get progressRatio() {
|
||||
return this.progress[0] / this.progress[1];
|
||||
}
|
||||
@@ -79,7 +91,7 @@ export default class FileReceiver extends Nanobus {
|
||||
this.state = 'downloading';
|
||||
this.downloadRequest = await downloadFile(
|
||||
this.fileInfo.id,
|
||||
this.keychain,
|
||||
this.dlToken,
|
||||
p => {
|
||||
this.progress = [p, this.fileInfo.size];
|
||||
this.emit('progress');
|
||||
@@ -143,6 +155,7 @@ export default class FileReceiver extends Nanobus {
|
||||
url: this.fileInfo.url,
|
||||
size: this.fileInfo.size,
|
||||
nonce: this.keychain.nonce,
|
||||
dlToken: this.dlToken,
|
||||
noSave
|
||||
};
|
||||
await this.sendMessageToSw(info);
|
||||
@@ -208,11 +221,19 @@ export default class FileReceiver extends Nanobus {
|
||||
}
|
||||
}
|
||||
|
||||
download(options) {
|
||||
if (options.stream) {
|
||||
return this.downloadStream(options.noSave);
|
||||
async download({ stream, storage, noSave }) {
|
||||
this.dlToken = storage.getDownloadToken(this.id);
|
||||
if (!this.dlToken) {
|
||||
this.dlToken = await getDownloadToken(this.id, this.keychain);
|
||||
storage.setDownloadToken(this.id, this.dlToken);
|
||||
}
|
||||
return this.downloadBlob(options.noSave);
|
||||
if (stream) {
|
||||
await this.downloadStream(noSave);
|
||||
} else {
|
||||
await this.downloadBlob(noSave);
|
||||
}
|
||||
await downloadDone(this.id, this.dlToken);
|
||||
storage.setDownloadToken(this.id);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ async function decryptStream(id) {
|
||||
keychain.setPassword(file.password, file.url);
|
||||
}
|
||||
|
||||
file.download = downloadStream(id, keychain);
|
||||
file.download = downloadStream(id, file.dlToken);
|
||||
|
||||
const body = await file.download.result;
|
||||
|
||||
@@ -146,6 +146,7 @@ self.onmessage = event => {
|
||||
type: event.data.type,
|
||||
manifest: event.data.manifest,
|
||||
size: event.data.size,
|
||||
dlToken: event.data.dlToken,
|
||||
progress: 0
|
||||
};
|
||||
map.set(event.data.id, info);
|
||||
|
||||
@@ -35,6 +35,7 @@ class Storage {
|
||||
this.engine = new Mem();
|
||||
}
|
||||
this._files = this.loadFiles();
|
||||
this.pruneTokens();
|
||||
}
|
||||
|
||||
loadFiles() {
|
||||
@@ -180,6 +181,48 @@ class Storage {
|
||||
downloadCount
|
||||
};
|
||||
}
|
||||
|
||||
setDownloadToken(id, token) {
|
||||
let otherTokens = {};
|
||||
try {
|
||||
otherTokens = JSON.parse(this.get('dlTokens'));
|
||||
} catch (e) {
|
||||
//
|
||||
}
|
||||
if (token) {
|
||||
const record = { token, ts: Date.now() };
|
||||
this.set('dlTokens', JSON.stringify({ ...otherTokens, [id]: record }));
|
||||
} else {
|
||||
this.set('dlTokens', JSON.stringify({ ...otherTokens, [id]: undefined }));
|
||||
}
|
||||
}
|
||||
|
||||
getDownloadToken(id) {
|
||||
try {
|
||||
return JSON.parse(this.get('dlTokens'))[id].token;
|
||||
} catch (e) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
pruneTokens() {
|
||||
try {
|
||||
const now = Date.now();
|
||||
const tokens = JSON.parse(this.get('dlTokens'));
|
||||
const keep = {};
|
||||
for (const id of Object.keys(tokens)) {
|
||||
const t = tokens[id];
|
||||
if (t.ts > now - 7 * 86400 * 1000) {
|
||||
keep[id] = t;
|
||||
}
|
||||
}
|
||||
if (Object.keys(keep).length < Object.keys(tokens).length) {
|
||||
this.set('dlTokens', JSON.stringify(keep));
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new Storage();
|
||||
|
||||
+1
-1
@@ -113,7 +113,7 @@ module.exports = function(state, emit) {
|
||||
<main class="main">
|
||||
${state.modal && modal(state, emit)}
|
||||
<section
|
||||
class="relative h-full w-full p-6 md:p-8 md:rounded-xl md:shadow-big md:flex md:flex-col"
|
||||
class="relative overflow-hidden h-full w-full p-6 md:p-8 md:rounded-xl md:shadow-big md:flex md:flex-col"
|
||||
>
|
||||
${content}
|
||||
</section>
|
||||
|
||||
Reference in New Issue
Block a user