import { arrayToB64, b64ToArray } from './utils'; function post(obj) { return { method: 'POST', headers: new Headers({ 'Content-Type': 'application/json' }), body: JSON.stringify(obj) }; } function parseNonce(header) { header = header || ''; return header.split(' ')[1]; } async function fetchWithAuth(url, params, keychain) { const result = {}; params = params || {}; const h = await keychain.authHeader(); params.headers = new Headers({ Authorization: h }); const response = await fetch(url, params); result.response = response; result.ok = response.ok; const nonce = parseNonce(response.headers.get('WWW-Authenticate')); result.shouldRetry = response.status === 401 && nonce !== keychain.nonce; keychain.nonce = nonce; return result; } async function fetchWithAuthAndRetry(url, params, keychain) { const result = await fetchWithAuth(url, params, keychain); if (result.shouldRetry) { return fetchWithAuth(url, params, keychain); } return result; } export async function del(id, owner_token) { const response = await fetch(`/api/delete/${id}`, post({ owner_token })); return response.ok; } export async function setParams(id, owner_token, params) { const response = await fetch( `/api/params/${id}`, post({ owner_token, dlimit: params.dlimit }) ); return response.ok; } export async function fileInfo(id, owner_token) { const response = await fetch(`/api/info/${id}`, post({ owner_token })); if (response.ok) { const obj = await response.json(); return obj; } throw new Error(response.status); } export async function metadata(id, keychain) { const result = await fetchWithAuthAndRetry( `/api/metadata/${id}`, { method: 'GET' }, keychain ); if (result.ok) { const data = await result.response.json(); const meta = await keychain.decryptMetadata(b64ToArray(data.metadata)); return { size: data.size, ttl: data.ttl, iv: meta.iv, name: meta.name, type: meta.type }; } throw new Error(result.response.status); } export async function setPassword(id, owner_token, keychain) { const auth = await keychain.authKeyB64(); const response = await fetch( `/api/password/${id}`, post({ owner_token, auth }) ); return response.ok; } export function uploadFile(encrypted, metadata, verifierB64, keychain) { const xhr = new XMLHttpRequest(); const upload = { onprogress: function() {}, cancel: function() { xhr.abort(); }, result: new Promise(function(resolve, reject) { xhr.addEventListener('loadend', function() { const authHeader = xhr.getResponseHeader('WWW-Authenticate'); if (authHeader) { keychain.nonce = parseNonce(authHeader); } if (xhr.status === 200) { const responseObj = JSON.parse(xhr.responseText); return resolve({ url: responseObj.url, id: responseObj.id, ownerToken: responseObj.owner }); } reject(new Error(xhr.status)); }); }) }; const dataView = new DataView(encrypted); const blob = new Blob([dataView], { type: 'application/octet-stream' }); const fd = new FormData(); fd.append('data', blob); xhr.upload.addEventListener('progress', function(event) { if (event.lengthComputable) { upload.onprogress([event.loaded, event.total]); } }); xhr.open('post', '/api/upload', true); xhr.setRequestHeader('X-File-Metadata', arrayToB64(new Uint8Array(metadata))); xhr.setRequestHeader('Authorization', `send-v1 ${verifierB64}`); xhr.send(fd); return upload; } function download(id, keychain) { const xhr = new XMLHttpRequest(); const download = { onprogress: function() {}, cancel: function() { xhr.abort(); }, result: new Promise(async function(resolve, reject) { xhr.addEventListener('loadend', function() { const authHeader = xhr.getResponseHeader('WWW-Authenticate'); if (authHeader) { keychain.nonce = parseNonce(authHeader); } if (xhr.status === 404) { return reject(new Error('notfound')); } if (xhr.status !== 200) { return reject(new Error(xhr.status)); } const blob = new Blob([xhr.response]); const fileReader = new FileReader(); fileReader.readAsArrayBuffer(blob); fileReader.onload = function() { resolve(this.result); }; }); xhr.addEventListener('progress', function(event) { if (event.lengthComputable && event.target.status === 200) { download.onprogress([event.loaded, event.total]); } }); const auth = await keychain.authHeader(); xhr.open('get', `/api/download/${id}`); xhr.setRequestHeader('Authorization', auth); xhr.responseType = 'blob'; xhr.send(); }) }; return download; } async function tryDownload(id, keychain, onprogress, tries = 1) { const dl = download(id, keychain); dl.onprogress = onprogress; try { const result = await dl.result; return result; } catch (e) { if (e.message === '401' && --tries > 0) { return tryDownload(id, keychain, onprogress, tries); } throw e; } } export function downloadFile(id, keychain) { let cancelled = false; function updateProgress(p) { if (cancelled) { // This is a bit of a hack // We piggyback off of the progress event as a chance to cancel. // Otherwise wiring the xhr abort up while allowing retries // gets pretty nasty. // 'this' here is the object returned by download(id, keychain) return this.cancel(); } dl.onprogress(p); } const dl = { onprogress: function() {}, cancel: function() { cancelled = true; }, result: tryDownload(id, keychain, updateProgress, 2) }; return dl; }