send/app/api.js

444 lines
10 KiB
JavaScript
Raw Normal View History

import { arrayToB64, b64ToArray, delay } from './utils';
2018-07-26 05:26:11 +00:00
import { ECE_RECORD_SIZE } from './ece';
2018-01-24 18:23:13 +00:00
let fileProtocolWssUrl = null;
try {
fileProtocolWssUrl = localStorage.getItem('wssURL');
} catch (e) {
// NOOP
}
if (!fileProtocolWssUrl) {
fileProtocolWssUrl = 'wss://send.firefox.com/api/ws';
}
export class ConnectionError extends Error {
constructor(cancelled, duration, size) {
super(cancelled ? '0' : 'connection closed');
this.cancelled = cancelled;
this.duration = duration;
this.size = size;
}
}
export function setFileProtocolWssUrl(url) {
localStorage && localStorage.setItem('wssURL', url);
fileProtocolWssUrl = url;
}
export function getFileProtocolWssUrl() {
return fileProtocolWssUrl;
}
let apiUrlPrefix = '';
export function getApiUrl(path) {
return apiUrlPrefix + path;
}
export function setApiUrlPrefix(prefix) {
apiUrlPrefix = prefix;
}
2018-08-31 17:59:26 +00:00
function post(obj, bearerToken) {
const h = {
'Content-Type': 'application/json'
};
if (bearerToken) {
h['Authentication'] = `Bearer ${bearerToken}`;
}
2018-01-24 18:23:13 +00:00
return {
method: 'POST',
2018-08-31 17:59:26 +00:00
headers: new Headers(h),
2018-01-24 18:23:13 +00:00
body: JSON.stringify(obj)
};
}
2018-07-09 22:39:06 +00:00
export function parseNonce(header) {
2018-01-24 18:23:13 +00:00
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,
'Content-Type': 'application/json'
});
2018-01-24 18:23:13 +00:00
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(
getApiUrl(`/api/delete/${id}`),
post({ owner_token })
);
2018-01-24 18:23:13 +00:00
return response.ok;
}
2018-08-31 17:59:26 +00:00
export async function setParams(id, owner_token, bearerToken, params) {
2018-01-24 18:23:13 +00:00
const response = await fetch(
getApiUrl(`/api/params/${id}`),
2018-08-31 17:59:26 +00:00
post(
{
owner_token,
dlimit: params.dlimit
},
bearerToken
)
2018-01-24 18:23:13 +00:00
);
return response.ok;
}
2018-01-30 20:15:09 +00:00
export async function fileInfo(id, owner_token) {
const response = await fetch(
getApiUrl(`/api/info/${id}`),
post({ owner_token })
);
2018-07-11 23:52:46 +00:00
2018-01-30 20:15:09 +00:00
if (response.ok) {
const obj = await response.json();
return obj;
}
2018-07-11 23:52:46 +00:00
2018-01-30 20:15:09 +00:00
throw new Error(response.status);
}
2018-01-24 18:23:13 +00:00
export async function metadata(id, keychain) {
const result = await fetchWithAuthAndRetry(
getApiUrl(`/api/metadata/${id}`),
2018-01-24 18:23:13 +00:00
{ method: 'GET' },
keychain
);
if (result.ok) {
const data = await result.response.json();
const meta = await keychain.decryptMetadata(b64ToArray(data.metadata));
return {
size: meta.size,
2018-01-24 18:23:13 +00:00
ttl: data.ttl,
iv: meta.iv,
name: meta.name,
2018-07-26 05:26:11 +00:00
type: meta.type,
manifest: meta.manifest
2018-01-24 18:23:13 +00:00
};
}
throw new Error(result.response.status);
}
export async function setPassword(id, owner_token, keychain) {
const auth = await keychain.authKeyB64();
const response = await fetch(
getApiUrl(`/api/password/${id}`),
2018-01-24 18:23:13 +00:00
post({ owner_token, auth })
);
return response.ok;
}
2018-06-21 00:05:33 +00:00
function asyncInitWebSocket(server) {
return new Promise((resolve, reject) => {
try {
const ws = new WebSocket(server);
ws.addEventListener('open', () => resolve(ws), { once: true });
} catch (e) {
reject(new ConnectionError(false));
}
2018-06-21 00:05:33 +00:00
});
}
2018-06-22 20:17:23 +00:00
function listenForResponse(ws, canceller) {
return new Promise((resolve, reject) => {
function handleClose(event) {
// a 'close' event before a 'message' event means the request failed
ws.removeEventListener('message', handleMessage);
reject(new ConnectionError(canceller.cancelled));
}
2018-08-31 21:20:15 +00:00
function handleMessage(msg) {
ws.removeEventListener('close', handleClose);
2018-06-22 20:17:23 +00:00
try {
2018-06-21 00:05:33 +00:00
const response = JSON.parse(msg.data);
2018-06-21 20:57:53 +00:00
if (response.error) {
2018-06-22 20:17:23 +00:00
throw new Error(response.error);
2018-06-21 20:57:53 +00:00
} else {
2018-08-31 21:20:15 +00:00
resolve(response);
2018-06-21 20:57:53 +00:00
}
2018-06-22 20:17:23 +00:00
} catch (e) {
reject(e);
}
2018-08-31 21:20:15 +00:00
}
ws.addEventListener('message', handleMessage, { once: true });
ws.addEventListener('close', handleClose, { once: true });
2018-06-22 20:17:23 +00:00
});
}
2018-06-21 00:05:33 +00:00
2018-08-08 18:07:09 +00:00
async function upload(
stream,
metadata,
verifierB64,
timeLimit,
2018-08-31 21:20:15 +00:00
dlimit,
2018-08-07 22:40:17 +00:00
bearerToken,
2018-08-08 18:07:09 +00:00
onprogress,
canceller
) {
let size = 0;
const start = Date.now();
2018-06-22 20:17:23 +00:00
const host = window.location.hostname;
const port = window.location.port;
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const endpoint =
window.location.protocol === 'file:'
? fileProtocolWssUrl
: `${protocol}//${host}${port ? ':' : ''}${port}/api/ws`;
const ws = await asyncInitWebSocket(endpoint);
2018-06-21 00:05:33 +00:00
2018-06-22 20:17:23 +00:00
try {
const metadataHeader = arrayToB64(new Uint8Array(metadata));
const fileMeta = {
fileMetadata: metadataHeader,
2018-08-08 18:07:09 +00:00
authorization: `send-v1 ${verifierB64}`,
2018-08-07 22:40:17 +00:00
bearer: bearerToken,
2018-08-31 21:20:15 +00:00
timeLimit,
dlimit
2018-06-22 20:17:23 +00:00
};
2018-08-31 21:20:15 +00:00
const uploadInfoResponse = listenForResponse(ws, canceller);
2018-06-22 20:17:23 +00:00
ws.send(JSON.stringify(fileMeta));
2018-08-31 21:20:15 +00:00
const uploadInfo = await uploadInfoResponse;
const completedResponse = listenForResponse(ws, canceller);
2018-06-21 00:05:33 +00:00
2018-06-22 20:17:23 +00:00
const reader = stream.getReader();
let state = await reader.read();
while (!state.done) {
if (canceller.cancelled) {
2019-08-07 20:47:59 +00:00
ws.close();
2018-06-22 20:17:23 +00:00
}
2019-08-07 20:47:59 +00:00
if (ws.readyState !== WebSocket.OPEN) {
break;
}
const buf = state.value;
2018-06-22 20:17:23 +00:00
ws.send(buf);
2018-07-26 05:26:11 +00:00
onprogress(size);
2018-07-09 22:39:06 +00:00
size += buf.length;
2018-06-22 20:17:23 +00:00
state = await reader.read();
while (
ws.bufferedAmount > ECE_RECORD_SIZE * 2 &&
2019-08-07 20:47:59 +00:00
ws.readyState === WebSocket.OPEN &&
!canceller.cancelled
) {
await delay();
}
2018-06-22 20:17:23 +00:00
}
2019-08-07 20:47:59 +00:00
if (ws.readyState === WebSocket.OPEN) {
ws.send(new Uint8Array([0])); //EOF
}
2018-06-22 20:17:23 +00:00
2018-08-31 21:20:15 +00:00
await completedResponse;
uploadInfo.duration = Date.now() - start;
2018-08-31 21:20:15 +00:00
return uploadInfo;
} catch (e) {
e.size = size;
e.duration = Date.now() - start;
throw e;
2019-08-07 20:47:59 +00:00
} finally {
if (![WebSocket.CLOSED, WebSocket.CLOSING].includes(ws.readyState)) {
ws.close();
}
2018-06-22 20:17:23 +00:00
}
2018-06-21 00:05:33 +00:00
}
2018-08-08 18:07:09 +00:00
export function uploadWs(
encrypted,
metadata,
verifierB64,
2018-08-07 22:40:17 +00:00
timeLimit,
2018-08-31 21:20:15 +00:00
dlimit,
2018-08-07 22:40:17 +00:00
bearerToken,
onprogress
2018-08-08 18:07:09 +00:00
) {
2018-06-21 20:57:53 +00:00
const canceller = { cancelled: false };
2018-06-21 00:05:33 +00:00
return {
2018-01-24 18:23:13 +00:00
cancel: function() {
2018-06-21 20:57:53 +00:00
canceller.cancelled = true;
2018-01-24 18:23:13 +00:00
},
2018-08-08 18:07:09 +00:00
result: upload(
encrypted,
metadata,
verifierB64,
timeLimit,
2018-08-31 21:20:15 +00:00
dlimit,
2018-08-07 22:40:17 +00:00
bearerToken,
2018-08-08 18:07:09 +00:00
onprogress,
canceller
)
2018-01-24 18:23:13 +00:00
};
}
2018-06-29 16:36:08 +00:00
////////////////////////
2018-07-06 22:49:50 +00:00
async function downloadS(id, keychain, signal) {
2018-06-29 16:36:08 +00:00
const auth = await keychain.authHeader();
const response = await fetch(getApiUrl(`/api/download/${id}`), {
2018-07-05 19:40:49 +00:00
signal: signal,
method: 'GET',
headers: { Authorization: auth }
});
2018-06-29 16:36:08 +00:00
2018-07-05 19:40:49 +00:00
const authHeader = response.headers.get('WWW-Authenticate');
if (authHeader) {
keychain.nonce = parseNonce(authHeader);
}
2018-06-29 16:36:08 +00:00
2018-07-11 23:52:46 +00:00
if (response.status !== 200) {
throw new Error(response.status);
2018-06-29 16:36:08 +00:00
}
2018-07-11 23:52:46 +00:00
return response.body;
2018-06-29 16:36:08 +00:00
}
async function tryDownloadStream(id, keychain, signal, tries = 2) {
2018-06-29 16:36:08 +00:00
try {
2018-07-06 22:49:50 +00:00
const result = await downloadS(id, keychain, signal);
2018-06-29 16:36:08 +00:00
return result;
} catch (e) {
if (e.message === '401' && --tries > 0) {
2018-07-06 22:49:50 +00:00
return tryDownloadStream(id, keychain, signal, tries);
2018-06-29 16:36:08 +00:00
}
2018-07-05 19:40:49 +00:00
if (e.name === 'AbortError') {
throw new Error('0');
}
2018-06-29 16:36:08 +00:00
throw e;
}
}
2018-07-06 22:49:50 +00:00
export function downloadStream(id, keychain) {
2018-06-29 16:36:08 +00:00
const controller = new AbortController();
function cancel() {
controller.abort();
}
return {
cancel,
result: tryDownloadStream(id, keychain, controller.signal)
2018-06-29 16:36:08 +00:00
};
}
//////////////////
2019-08-02 19:03:53 +00:00
async function download(id, keychain, onprogress, canceller) {
const auth = await keychain.authHeader();
2018-01-24 18:23:13 +00:00
const xhr = new XMLHttpRequest();
canceller.oncancel = function() {
xhr.abort();
2018-01-24 18:23:13 +00:00
};
2019-08-02 19:03:53 +00:00
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));
}
2018-01-24 18:23:13 +00:00
const blob = new Blob([xhr.response]);
2018-06-21 00:05:33 +00:00
resolve(blob);
});
2018-07-12 23:07:18 +00:00
xhr.addEventListener('progress', function(event) {
2018-11-16 21:33:40 +00:00
if (event.target.status === 200) {
onprogress(event.loaded);
}
});
xhr.open('get', getApiUrl(`/api/download/blob/${id}`));
xhr.setRequestHeader('Authorization', auth);
xhr.responseType = 'blob';
xhr.send();
2018-11-16 21:33:40 +00:00
onprogress(0);
});
2018-01-24 18:23:13 +00:00
}
async function tryDownload(id, keychain, onprogress, canceller, tries = 2) {
2018-01-24 18:23:13 +00:00
try {
const result = await download(id, keychain, onprogress, canceller);
2018-01-24 18:23:13 +00:00
return result;
} catch (e) {
if (e.message === '401' && --tries > 0) {
return tryDownload(id, keychain, onprogress, canceller, tries);
2018-01-24 18:23:13 +00:00
}
throw e;
}
}
export function downloadFile(id, keychain, onprogress) {
const canceller = {
oncancel: function() {} // download() sets this
};
function cancel() {
canceller.oncancel();
2018-01-24 18:23:13 +00:00
}
return {
cancel,
result: tryDownload(id, keychain, onprogress, canceller)
2018-01-24 18:23:13 +00:00
};
}
2018-08-07 22:40:17 +00:00
export async function getFileList(bearerToken, kid) {
2018-08-07 22:40:17 +00:00
const headers = new Headers({ Authorization: `Bearer ${bearerToken}` });
const response = await fetch(getApiUrl(`/api/filelist/${kid}`), { headers });
if (response.ok) {
const encrypted = await response.blob();
return encrypted;
}
throw new Error(response.status);
2018-08-07 22:40:17 +00:00
}
export async function setFileList(bearerToken, kid, data) {
2018-08-07 22:40:17 +00:00
const headers = new Headers({ Authorization: `Bearer ${bearerToken}` });
const response = await fetch(getApiUrl(`/api/filelist/${kid}`), {
2018-08-07 22:40:17 +00:00
headers,
method: 'POST',
body: data
});
return response.ok;
2018-08-07 22:40:17 +00:00
}
export function sendMetrics(blob) {
if (!navigator.sendBeacon) {
return;
}
try {
navigator.sendBeacon(getApiUrl('/api/metrics'), blob);
} catch (e) {
console.error(e);
}
}
export async function getConstants() {
const response = await fetch(getApiUrl('/config'));
if (response.ok) {
const obj = await response.json();
return obj;
}
throw new Error(response.status);
}