diff --git a/.circleci/config.yml b/.circleci/config.yml
index b7e6234f9..3cf275e6c 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -1,77 +1,48 @@
version: 2.0
jobs:
- build:
- docker:
- - image: circleci/node:10
- steps:
- - checkout
- - restore_cache:
- key: send-build-{{ checksum "package-lock.json" }}
- - run: npm install
- - save_cache:
- key: send-build-{{ checksum "package-lock.json" }}
- paths:
- - node_modules
- - run: npm run build
- - persist_to_workspace:
- root: .
- paths:
- - ./dist
test:
docker:
- - image: circleci/node:10-browsers
+ - image: circleci/node:12-browsers
steps:
- checkout
- - restore_cache:
- key: send-test-{{ checksum "package-lock.json" }}
- - run: npm install
- - save_cache:
- key: send-test-{{ checksum "package-lock.json" }}
- paths:
- - node_modules
+ - run: npm ci
- run: npm run lint
- - run: npm run test
+ - run: npm test
- store_artifacts:
path: coverage
integration_tests:
docker:
- - image: circleci/node:10-browsers
+ - image: circleci/node:12-browsers
steps:
- checkout
- - restore_cache:
- key: send-int-{{ checksum "package-lock.json" }}
- - run: npm install
- - save_cache:
- key: send-int-{{ checksum "package-lock.json" }}
- paths:
- - node_modules
- - run:
+ - run: npm ci
+ - run:
name: Run integration test
command: ./scripts/bin/run-integration-test-circleci.sh
deploy_dev:
- machine: true
+ docker:
+ - image: circleci/node:12
steps:
- checkout
- - attach_workspace:
- at: .
+ - setup_remote_docker
- run: docker login -u $DOCKER_USER -p $DOCKER_PASS
- run: docker build -t mozilla/send:latest .
- run: docker push mozilla/send:latest
deploy_vnext:
- machine: true
+ docker:
+ - image: circleci/node:12
steps:
- checkout
- - attach_workspace:
- at: .
+ - setup_remote_docker
- run: docker login -u $DOCKER_USER -p $DOCKER_PASS
- run: docker build -t mozilla/send:vnext .
- run: docker push mozilla/send:vnext
deploy_stage:
- machine: true
+ docker:
+ - image: circleci/node:12
steps:
- checkout
- - attach_workspace:
- at: .
+ - setup_remote_docker
- run: docker login -u $DOCKER_USER -p $DOCKER_PASS
- run: docker build -t mozilla/send:$CIRCLE_TAG .
- run: docker push mozilla/send:$CIRCLE_TAG
@@ -79,12 +50,6 @@ workflows:
version: 2
test_pr:
jobs:
- - build:
- filters:
- branches:
- ignore:
- - master
- - vnext
- test:
filters:
branches:
@@ -97,25 +62,13 @@ workflows:
ignore: master
build_and_deploy_dev:
jobs:
- - build:
- filters:
- branches:
- only:
- - master
- - vnext
- tags:
- ignore: /^v.*/
- deploy_dev:
- requires:
- - build
filters:
branches:
only: master
tags:
ignore: /^v.*/
- deploy_vnext:
- requires:
- - build
filters:
branches:
only: vnext
@@ -123,12 +76,6 @@ workflows:
ignore: /^v.*/
build_and_deploy_stage:
jobs:
- - build:
- filters:
- branches:
- ignore: /.*/
- tags:
- only: /^v.*/
- test:
filters:
branches:
@@ -143,7 +90,6 @@ workflows:
only: /^v.*/
- deploy_stage:
requires:
- - build
- test
- integration_tests
filters:
diff --git a/Dockerfile b/Dockerfile
index 5570c2f22..064053ed0 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -6,7 +6,7 @@
# Build project
-FROM node:10 AS builder
+FROM node:12 AS builder
RUN set -x \
# Add user
&& addgroup --gid 10001 app \
@@ -19,15 +19,14 @@ RUN set -x \
COPY --chown=app:app . /app
USER app
WORKDIR /app
-RUN ls -la
RUN set -x \
# Build
- && npm ci \
+ && PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true npm ci \
&& npm run build
# Main image
-FROM node:10-slim
+FROM node:12-slim
RUN set -x \
# Add user
&& addgroup --gid 10001 app \
@@ -37,7 +36,9 @@ RUN set -x \
--home /app \
--uid 10001 \
app
-RUN apt-get update && apt-get -y install git-core
+RUN apt-get update && apt-get -y install \
+ git-core \
+ && rm -rf /var/lib/apt/lists/*
USER app
WORKDIR /app
COPY --chown=app:app package*.json ./
@@ -47,7 +48,6 @@ COPY --chown=app:app public/locales public/locales
COPY --chown=app:app server server
COPY --chown=app:app --from=builder /app/dist dist
-RUN ls -la
RUN npm ci --production && npm cache clean --force
RUN mkdir -p /app/.config/configstore
RUN ln -s dist/version.json version.json
diff --git a/README.md b/README.md
index 3a2014afa..68f721030 100644
--- a/README.md
+++ b/README.md
@@ -30,22 +30,22 @@ A file sharing experiment which allows you to send encrypted files to other user
## Requirements
-- [Node.js 10.x](https://nodejs.org/)
+- [Node.js 12.x](https://nodejs.org/)
- [Redis server](https://redis.io/) (optional for development)
-- [AWS S3](https://aws.amazon.com/s3/) or compatible service. (optional)
+- [AWS S3](https://aws.amazon.com/s3/) or compatible service (optional)
---
## Development
-To start an ephemeral development server run:
+To start an ephemeral development server, run:
```sh
npm install
npm start
```
-Then browse to http://localhost:8080
+Then, browse to http://localhost:8080
---
diff --git a/app/api.js b/app/api.js
index fd29db777..c316b1cb7 100644
--- a/app/api.js
+++ b/app/api.js
@@ -61,7 +61,10 @@ async function fetchWithAuth(url, params, keychain) {
const result = {};
params = params || {};
const h = await keychain.authHeader();
- params.headers = new Headers({ Authorization: h });
+ params.headers = new Headers({
+ Authorization: h,
+ 'Content-Type': 'application/json'
+ });
const response = await fetch(url, params);
result.response = response;
result.ok = response.ok;
@@ -127,10 +130,10 @@ export async function metadata(id, keychain) {
return {
size: meta.size,
ttl: data.ttl,
- iv: meta.iv,
name: meta.name,
type: meta.type,
- manifest: meta.manifest
+ manifest: meta.manifest,
+ flagged: data.flagged
};
}
throw new Error(result.response.status);
@@ -289,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);
}
@@ -310,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');
@@ -325,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();
@@ -347,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));
}
@@ -365,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
};
@@ -393,7 +384,7 @@ export function downloadFile(id, keychain, onprogress) {
}
return {
cancel,
- result: tryDownload(id, keychain, onprogress, canceller)
+ result: tryDownload(id, dlToken, onprogress, canceller)
};
}
@@ -438,3 +429,44 @@ export async function getConstants() {
throw new Error(response.status);
}
+
+export async function reportLink(id, keychain, reason) {
+ const result = await fetchWithAuthAndRetry(
+ getApiUrl(`/api/report/${id}`),
+ {
+ method: 'POST',
+ body: JSON.stringify({ reason })
+ },
+ keychain
+ );
+
+ if (result.ok) {
+ return;
+ }
+
+ 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;
+}
diff --git a/app/capabilities.js b/app/capabilities.js
index d37e9e05b..d43a6b108 100644
--- a/app/capabilities.js
+++ b/app/capabilities.js
@@ -77,6 +77,7 @@ async function polyfillStreams() {
export default async function getCapabilities() {
const browser = browserName();
+ const isMobile = /mobi|android/i.test(navigator.userAgent);
const serviceWorker = 'serviceWorker' in navigator && browser !== 'edge';
let crypto = await checkCrypto();
const nativeStreams = checkStreams();
@@ -91,14 +92,15 @@ export default async function getCapabilities() {
account = false;
}
const share =
- typeof navigator.share === 'function' && locale().startsWith('en'); // en until strings merge
+ isMobile &&
+ typeof navigator.share === 'function' &&
+ locale().startsWith('en'); // en until strings merge
const standalone =
window.matchMedia('(display-mode: standalone)').matches ||
navigator.standalone;
- const mobileFirefox =
- browser === 'firefox' && /mobile/i.test(navigator.userAgent);
+ const mobileFirefox = browser === 'firefox' && isMobile;
return {
account,
diff --git a/app/controller.js b/app/controller.js
index b779f3603..b5f97cc45 100644
--- a/app/controller.js
+++ b/app/controller.js
@@ -49,8 +49,8 @@ export default function(state, emitter) {
state.user.login(email);
});
- emitter.on('logout', () => {
- state.user.logout();
+ emitter.on('logout', async () => {
+ await state.user.logout();
metrics.loggedOut({ trigger: 'button' });
emitter.emit('pushState', '/');
});
@@ -178,6 +178,12 @@ export default function(state, emitter) {
//cancelled. do nothing
metrics.cancelledUpload(archive, err.duration);
render();
+ } else if (err.message === '401') {
+ const refreshed = await state.user.refresh();
+ if (refreshed) {
+ return emitter.emit('upload');
+ }
+ emitter.emit('pushState', '/error');
} else {
// eslint-disable-next-line no-console
console.error(err);
@@ -226,9 +232,10 @@ export default function(state, emitter) {
} catch (e) {
if (e.message === '401' || e.message === '404') {
file.password = null;
- if (!file.requiresPassword) {
- return emitter.emit('pushState', '/404');
- }
+ file.dead = e.message === '404';
+ } else {
+ console.error(e);
+ return emitter.emit('pushState', '/error');
}
}
@@ -244,7 +251,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;
@@ -263,7 +271,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);
@@ -306,6 +316,21 @@ export default function(state, emitter) {
render();
});
+ emitter.on('report', async ({ reason }) => {
+ try {
+ const receiver = state.transfer || new FileReceiver(state.fileInfo);
+ await receiver.reportLink(reason);
+ render();
+ } catch (err) {
+ console.error(err);
+ if (err.message === '404') {
+ state.fileInfo = { reported: true };
+ return render();
+ }
+ emitter.emit('pushState', '/error');
+ }
+ });
+
setInterval(() => {
// poll for updates of the upload list
if (!state.modal && state.route === '/') {
diff --git a/app/crc32.js b/app/crc32.js
new file mode 100644
index 000000000..ec6d67e67
--- /dev/null
+++ b/app/crc32.js
@@ -0,0 +1,266 @@
+const LOOKUP = Int32Array.from([
+ 0x00000000,
+ 0x77073096,
+ 0xee0e612c,
+ 0x990951ba,
+ 0x076dc419,
+ 0x706af48f,
+ 0xe963a535,
+ 0x9e6495a3,
+ 0x0edb8832,
+ 0x79dcb8a4,
+ 0xe0d5e91e,
+ 0x97d2d988,
+ 0x09b64c2b,
+ 0x7eb17cbd,
+ 0xe7b82d07,
+ 0x90bf1d91,
+ 0x1db71064,
+ 0x6ab020f2,
+ 0xf3b97148,
+ 0x84be41de,
+ 0x1adad47d,
+ 0x6ddde4eb,
+ 0xf4d4b551,
+ 0x83d385c7,
+ 0x136c9856,
+ 0x646ba8c0,
+ 0xfd62f97a,
+ 0x8a65c9ec,
+ 0x14015c4f,
+ 0x63066cd9,
+ 0xfa0f3d63,
+ 0x8d080df5,
+ 0x3b6e20c8,
+ 0x4c69105e,
+ 0xd56041e4,
+ 0xa2677172,
+ 0x3c03e4d1,
+ 0x4b04d447,
+ 0xd20d85fd,
+ 0xa50ab56b,
+ 0x35b5a8fa,
+ 0x42b2986c,
+ 0xdbbbc9d6,
+ 0xacbcf940,
+ 0x32d86ce3,
+ 0x45df5c75,
+ 0xdcd60dcf,
+ 0xabd13d59,
+ 0x26d930ac,
+ 0x51de003a,
+ 0xc8d75180,
+ 0xbfd06116,
+ 0x21b4f4b5,
+ 0x56b3c423,
+ 0xcfba9599,
+ 0xb8bda50f,
+ 0x2802b89e,
+ 0x5f058808,
+ 0xc60cd9b2,
+ 0xb10be924,
+ 0x2f6f7c87,
+ 0x58684c11,
+ 0xc1611dab,
+ 0xb6662d3d,
+ 0x76dc4190,
+ 0x01db7106,
+ 0x98d220bc,
+ 0xefd5102a,
+ 0x71b18589,
+ 0x06b6b51f,
+ 0x9fbfe4a5,
+ 0xe8b8d433,
+ 0x7807c9a2,
+ 0x0f00f934,
+ 0x9609a88e,
+ 0xe10e9818,
+ 0x7f6a0dbb,
+ 0x086d3d2d,
+ 0x91646c97,
+ 0xe6635c01,
+ 0x6b6b51f4,
+ 0x1c6c6162,
+ 0x856530d8,
+ 0xf262004e,
+ 0x6c0695ed,
+ 0x1b01a57b,
+ 0x8208f4c1,
+ 0xf50fc457,
+ 0x65b0d9c6,
+ 0x12b7e950,
+ 0x8bbeb8ea,
+ 0xfcb9887c,
+ 0x62dd1ddf,
+ 0x15da2d49,
+ 0x8cd37cf3,
+ 0xfbd44c65,
+ 0x4db26158,
+ 0x3ab551ce,
+ 0xa3bc0074,
+ 0xd4bb30e2,
+ 0x4adfa541,
+ 0x3dd895d7,
+ 0xa4d1c46d,
+ 0xd3d6f4fb,
+ 0x4369e96a,
+ 0x346ed9fc,
+ 0xad678846,
+ 0xda60b8d0,
+ 0x44042d73,
+ 0x33031de5,
+ 0xaa0a4c5f,
+ 0xdd0d7cc9,
+ 0x5005713c,
+ 0x270241aa,
+ 0xbe0b1010,
+ 0xc90c2086,
+ 0x5768b525,
+ 0x206f85b3,
+ 0xb966d409,
+ 0xce61e49f,
+ 0x5edef90e,
+ 0x29d9c998,
+ 0xb0d09822,
+ 0xc7d7a8b4,
+ 0x59b33d17,
+ 0x2eb40d81,
+ 0xb7bd5c3b,
+ 0xc0ba6cad,
+ 0xedb88320,
+ 0x9abfb3b6,
+ 0x03b6e20c,
+ 0x74b1d29a,
+ 0xead54739,
+ 0x9dd277af,
+ 0x04db2615,
+ 0x73dc1683,
+ 0xe3630b12,
+ 0x94643b84,
+ 0x0d6d6a3e,
+ 0x7a6a5aa8,
+ 0xe40ecf0b,
+ 0x9309ff9d,
+ 0x0a00ae27,
+ 0x7d079eb1,
+ 0xf00f9344,
+ 0x8708a3d2,
+ 0x1e01f268,
+ 0x6906c2fe,
+ 0xf762575d,
+ 0x806567cb,
+ 0x196c3671,
+ 0x6e6b06e7,
+ 0xfed41b76,
+ 0x89d32be0,
+ 0x10da7a5a,
+ 0x67dd4acc,
+ 0xf9b9df6f,
+ 0x8ebeeff9,
+ 0x17b7be43,
+ 0x60b08ed5,
+ 0xd6d6a3e8,
+ 0xa1d1937e,
+ 0x38d8c2c4,
+ 0x4fdff252,
+ 0xd1bb67f1,
+ 0xa6bc5767,
+ 0x3fb506dd,
+ 0x48b2364b,
+ 0xd80d2bda,
+ 0xaf0a1b4c,
+ 0x36034af6,
+ 0x41047a60,
+ 0xdf60efc3,
+ 0xa867df55,
+ 0x316e8eef,
+ 0x4669be79,
+ 0xcb61b38c,
+ 0xbc66831a,
+ 0x256fd2a0,
+ 0x5268e236,
+ 0xcc0c7795,
+ 0xbb0b4703,
+ 0x220216b9,
+ 0x5505262f,
+ 0xc5ba3bbe,
+ 0xb2bd0b28,
+ 0x2bb45a92,
+ 0x5cb36a04,
+ 0xc2d7ffa7,
+ 0xb5d0cf31,
+ 0x2cd99e8b,
+ 0x5bdeae1d,
+ 0x9b64c2b0,
+ 0xec63f226,
+ 0x756aa39c,
+ 0x026d930a,
+ 0x9c0906a9,
+ 0xeb0e363f,
+ 0x72076785,
+ 0x05005713,
+ 0x95bf4a82,
+ 0xe2b87a14,
+ 0x7bb12bae,
+ 0x0cb61b38,
+ 0x92d28e9b,
+ 0xe5d5be0d,
+ 0x7cdcefb7,
+ 0x0bdbdf21,
+ 0x86d3d2d4,
+ 0xf1d4e242,
+ 0x68ddb3f8,
+ 0x1fda836e,
+ 0x81be16cd,
+ 0xf6b9265b,
+ 0x6fb077e1,
+ 0x18b74777,
+ 0x88085ae6,
+ 0xff0f6a70,
+ 0x66063bca,
+ 0x11010b5c,
+ 0x8f659eff,
+ 0xf862ae69,
+ 0x616bffd3,
+ 0x166ccf45,
+ 0xa00ae278,
+ 0xd70dd2ee,
+ 0x4e048354,
+ 0x3903b3c2,
+ 0xa7672661,
+ 0xd06016f7,
+ 0x4969474d,
+ 0x3e6e77db,
+ 0xaed16a4a,
+ 0xd9d65adc,
+ 0x40df0b66,
+ 0x37d83bf0,
+ 0xa9bcae53,
+ 0xdebb9ec5,
+ 0x47b2cf7f,
+ 0x30b5ffe9,
+ 0xbdbdf21c,
+ 0xcabac28a,
+ 0x53b39330,
+ 0x24b4a3a6,
+ 0xbad03605,
+ 0xcdd70693,
+ 0x54de5729,
+ 0x23d967bf,
+ 0xb3667a2e,
+ 0xc4614ab8,
+ 0x5d681b02,
+ 0x2a6f2b94,
+ 0xb40bbe37,
+ 0xc30c8ea1,
+ 0x5a05df1b,
+ 0x2d02ef8d
+]);
+
+module.exports = function crc32(uint8Array, previous) {
+ let crc = previous === 0 ? 0 : ~~previous ^ -1;
+ for (let i = 0; i < uint8Array.byteLength; i++) {
+ crc = LOOKUP[(crc ^ uint8Array[i]) & 0xff] ^ (crc >>> 8);
+ }
+ return (crc ^ -1) >>> 0;
+};
diff --git a/app/ece.js b/app/ece.js
index 4cd6b45e2..7fff87b4c 100644
--- a/app/ece.js
+++ b/app/ece.js
@@ -1,5 +1,5 @@
-import 'buffer';
import { transformStream } from './streams';
+import { concat } from './utils';
const NONCE_LENGTH = 12;
const TAG_LENGTH = 16;
@@ -81,19 +81,18 @@ class ECETransformer {
)
);
- return Buffer.from(base.slice(0, NONCE_LENGTH));
+ return base.slice(0, NONCE_LENGTH);
}
generateNonce(seq) {
if (seq > 0xffffffff) {
throw new Error('record sequence number exceeds limit');
}
- const nonce = Buffer.from(this.nonceBase);
- const m = nonce.readUIntBE(nonce.length - 4, 4);
+ const nonce = new DataView(this.nonceBase.slice());
+ const m = nonce.getUint32(nonce.byteLength - 4);
const xor = (m ^ seq) >>> 0; //forces unsigned int xor
- nonce.writeUIntBE(xor, nonce.length - 4, 4);
-
- return nonce;
+ nonce.setUint32(nonce.byteLength - 4, xor);
+ return new Uint8Array(nonce.buffer);
}
pad(data, isLast) {
@@ -103,14 +102,11 @@ class ECETransformer {
}
if (isLast) {
- const padding = Buffer.alloc(1);
- padding.writeUInt8(2, 0);
- return Buffer.concat([data, padding]);
+ return concat(data, Uint8Array.of(2));
} else {
- const padding = Buffer.alloc(this.rs - len - TAG_LENGTH);
- padding.fill(0);
- padding.writeUInt8(1, 0);
- return Buffer.concat([data, padding]);
+ const padding = new Uint8Array(this.rs - len - TAG_LENGTH);
+ padding[0] = 1;
+ return concat(data, padding);
}
}
@@ -133,10 +129,9 @@ class ECETransformer {
}
createHeader() {
- const nums = Buffer.alloc(5);
- nums.writeUIntBE(this.rs, 0, 4);
- nums.writeUIntBE(0, 4, 1);
- return Buffer.concat([Buffer.from(this.salt), nums]);
+ const nums = new DataView(new ArrayBuffer(5));
+ nums.setUint32(0, this.rs);
+ return concat(new Uint8Array(this.salt), new Uint8Array(nums.buffer));
}
readHeader(buffer) {
@@ -144,9 +139,10 @@ class ECETransformer {
throw new Error('chunk too small for reading header');
}
const header = {};
- header.salt = buffer.buffer.slice(0, KEY_LENGTH);
- header.rs = buffer.readUIntBE(KEY_LENGTH, 4);
- const idlen = buffer.readUInt8(KEY_LENGTH + 4);
+ const dv = new DataView(buffer.buffer);
+ header.salt = buffer.slice(0, KEY_LENGTH);
+ header.rs = dv.getUint32(KEY_LENGTH);
+ const idlen = dv.getUint8(KEY_LENGTH + 4);
header.length = idlen + KEY_LENGTH + 5;
return header;
}
@@ -158,7 +154,7 @@ class ECETransformer {
this.key,
this.pad(buffer, isLast)
);
- return Buffer.from(encrypted);
+ return new Uint8Array(encrypted);
}
async decryptRecord(buffer, seq, isLast) {
@@ -173,7 +169,7 @@ class ECETransformer {
buffer
);
- return this.unpad(Buffer.from(data), isLast);
+ return this.unpad(new Uint8Array(data), isLast);
}
async start(controller) {
@@ -214,7 +210,7 @@ class ECETransformer {
await this.transformPrevChunk(false, controller);
}
this.firstchunk = false;
- this.prevChunk = Buffer.from(chunk.buffer);
+ this.prevChunk = new Uint8Array(chunk.buffer);
}
async flush(controller) {
diff --git a/app/fileReceiver.js b/app/fileReceiver.js
index 262b19e16..764480468 100644
--- a/app/fileReceiver.js
+++ b/app/fileReceiver.js
@@ -1,7 +1,14 @@
import Nanobus from 'nanobus';
import Keychain from './keychain';
import { delay, bytes, streamToArrayBuffer } from './utils';
-import { downloadFile, metadata, getApiUrl } 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];
}
@@ -47,12 +59,16 @@ export default class FileReceiver extends Nanobus {
const meta = await metadata(this.fileInfo.id, this.keychain);
this.fileInfo.name = meta.name;
this.fileInfo.type = meta.type;
- this.fileInfo.iv = meta.iv;
this.fileInfo.size = +meta.size;
this.fileInfo.manifest = meta.manifest;
+ this.fileInfo.flagged = meta.flagged;
this.state = 'ready';
}
+ async reportLink(reason) {
+ await reportLink(this.fileInfo.id, this.keychain, reason);
+ }
+
sendMessageToSw(msg) {
return new Promise((resolve, reject) => {
const channel = new MessageChannel();
@@ -75,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');
@@ -139,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);
@@ -204,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);
+ }
+ if (stream) {
+ await this.downloadStream(noSave);
+ } else {
+ await this.downloadBlob(noSave);
}
- return this.downloadBlob(options.noSave);
+ await downloadDone(this.id, this.dlToken);
+ storage.setDownloadToken(this.id);
}
}
diff --git a/app/fxa.js b/app/fxa.js
index 7827d33e9..6af7da275 100644
--- a/app/fxa.js
+++ b/app/fxa.js
@@ -1,5 +1,5 @@
/* global AUTH_CONFIG */
-import { arrayToB64, b64ToArray } from './utils';
+import { arrayToB64, b64ToArray, concat } from './utils';
const encoder = new TextEncoder();
const decoder = new TextDecoder();
@@ -23,13 +23,6 @@ function getOtherInfo(enc) {
return result;
}
-function concat(b1, b2) {
- const result = new Uint8Array(b1.length + b2.length);
- result.set(b1, 0);
- result.set(b2, b1.length);
- return result;
-}
-
async function concatKdf(key, enc) {
if (key.length !== 32) {
throw new Error('unsupported key length');
diff --git a/app/main.css b/app/main.css
index 7a1dcaeda..1dd68252c 100644
--- a/app/main.css
+++ b/app/main.css
@@ -55,6 +55,12 @@ body {
@apply bg-blue-70;
}
+.btn:disabled {
+ @apply bg-grey-transparent;
+
+ cursor: not-allowed;
+}
+
.checkbox {
@apply leading-normal;
@apply select-none;
@@ -138,21 +144,6 @@ footer li:hover {
text-decoration: underline;
}
-.feedback-link {
- background-color: #000;
- background-image: url('../assets/feedback.svg');
- background-position: 0.125rem 0.25rem;
- background-repeat: no-repeat;
- background-size: 1.125rem;
- color: #fff;
- display: block;
- font-size: 0.75rem;
- line-height: 0.75rem;
- padding: 0.375rem 0.375rem 0.375rem 1.25rem;
- text-indent: 0.125rem;
- white-space: nowrap;
-}
-
.link-blue {
@apply text-blue-60;
}
@@ -174,6 +165,10 @@ footer li:hover {
padding-top:5%
}
+.dl-bg {
+ filter: grayscale(1) opacity(0.15);
+}
+
.main {
display: flex;
position: relative;
@@ -295,7 +290,7 @@ select {
@apply m-auto;
@apply py-8;
- min-height: 36rem;
+ min-height: 42rem;
max-height: 42rem;
width: calc(100% - 3rem);
}
@@ -321,6 +316,10 @@ select {
@apply bg-blue-50;
}
+ .btn:disabled {
+ @apply bg-grey-80;
+ }
+
.link-blue {
@apply text-blue-40;
}
@@ -391,48 +390,3 @@ select {
.signin:hover:active {
transform: scale(0.9375);
}
-
-/* begin signin button color experiment */
-
-.white-blue {
- @apply border-blue-60;
- @apply border-2;
- @apply text-blue-60;
-}
-
-.white-blue:hover,
-.white-blue:focus {
- @apply bg-blue-60;
- @apply text-white;
-}
-
-.blue {
- @apply bg-blue-60;
- @apply text-white;
-}
-
-.white-violet {
- @apply border-violet;
- @apply border-2;
- @apply text-violet;
-}
-
-.white-violet:hover,
-.white-violet:focus {
- @apply bg-violet;
- @apply text-white;
-
- background-image: var(--violet-gradient);
-}
-
-.violet {
- @apply bg-violet;
- @apply text-white;
-}
-
-.violet:hover,
-.violet:focus {
- background-image: var(--violet-gradient);
-}
-
-/* end signin button color experiment */
diff --git a/app/main.js b/app/main.js
index 2657746b0..1519a1225 100644
--- a/app/main.js
+++ b/app/main.js
@@ -59,7 +59,8 @@ if (process.env.NODE_ENV === 'production') {
sentry: Sentry,
user: new User(storage, LIMITS, window.AUTH_CONFIG),
transfer: null,
- fileInfo: null
+ fileInfo: null,
+ locale: locale()
};
const app = routes(choo({ hash: true }));
diff --git a/app/readme.md b/app/readme.md
index 7708988a3..b80e52469 100644
--- a/app/readme.md
+++ b/app/readme.md
@@ -2,7 +2,7 @@
`app/` contains the browser code that gets bundled into `app.[hash].js`. It's got all the logic, crypto, and UI. All of it gets used in the browser, and some of it by the server for server side rendering.
-The main entrypoint for the browser is [main.js](./main.js) and on the server [routes/index.js](./routes/index.js) gets imported by [/server/routes/pages.js](../server/routes/pages.js)
+The main entrypoint for the browser is [main.js](./main.js) and on the server [routes.js](./routes.js) is imported by [/server/routes/pages.js](../server/routes/pages.js)
- `pages` contains display logic an markup for pages
- `routes` contains route definitions and logic
diff --git a/app/routes.js b/app/routes.js
index 1ba8d412c..75eb9e0f1 100644
--- a/app/routes.js
+++ b/app/routes.js
@@ -13,7 +13,11 @@ module.exports = function(app = choo({ hash: true })) {
app.route('/oauth', function(state, emit) {
emit('authenticate', state.query.code, state.query.state);
});
- app.route('/login', body(require('./ui/home')));
+ app.route('/login', function(state, emit) {
+ emit('replaceState', '/');
+ setTimeout(() => emit('render'));
+ });
+ app.route('/report', body(require('./ui/report')));
app.route('*', body(require('./ui/notFound')));
return app;
};
diff --git a/app/serviceWorker.js b/app/serviceWorker.js
index bc824e2d3..9e32630e6 100644
--- a/app/serviceWorker.js
+++ b/app/serviceWorker.js
@@ -9,7 +9,7 @@ import contentDisposition from 'content-disposition';
let noSave = false;
const map = new Map();
const IMAGES = /.*\.(png|svg|jpg)$/;
-const VERSIONED_ASSET = /\.[A-Fa-f0-9]{8}\.(js|css|png|svg|jpg)$/;
+const VERSIONED_ASSET = /\.[A-Fa-f0-9]{8}\.(js|css|png|svg|jpg)(#\w+)?$/;
const DOWNLOAD_URL = /\/api\/download\/([A-Fa-f0-9]{4,})/;
const FONT = /\.woff2?$/;
@@ -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);
diff --git a/app/storage.js b/app/storage.js
index 304759ea4..a4a369a6d 100644
--- a/app/storage.js
+++ b/app/storage.js
@@ -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();
diff --git a/app/ui/account.js b/app/ui/account.js
index a81117e7c..7f6430ec2 100644
--- a/app/ui/account.js
+++ b/app/ui/account.js
@@ -54,12 +54,17 @@ class Account extends Component {
createElement() {
if (!this.enabled) {
return html`
-
+
`;
}
const user = this.state.user;
const translate = this.state.translate;
this.setLocal();
+ if (user.loginRequired && !this.local.loggedIn) {
+ return html`
+
+ `;
+ }
if (!this.local.loggedIn) {
return html`
diff --git a/app/ui/archiveTile.js b/app/ui/archiveTile.js
index 35ca0f9de..40672f95b 100644
--- a/app/ui/archiveTile.js
+++ b/app/ui/archiveTile.js
@@ -30,6 +30,12 @@ function password(state) {
return html`
+
${state.translate('addFilesButton')}
+
+ ${state.translate('trustWarningMessage')}
+
${upsell}
`;
@@ -517,13 +528,27 @@ module.exports.preview = function(state, emit) {
`;
return html`
-
+
${archiveInfo(archive)} ${details}
+
+
+
+
+ `;
+ }
if (!state.capabilities.streamDownload && state.fileInfo.size > BIG_SIZE) {
return noStreams(state, emit);
}
return html`
-
-
- ${state.translate('downloadTitle')}
-
-
+
- ${state.translate('downloadDescription')}
-
- ${archiveTile.preview(state, emit)}
+
+ ${state.translate('downloadTitle')}
+
+
+ ${state.translate('downloadDescription')}
+
+
+ ${state.translate('downloadConfirmDescription')}
+
+
+
+
`;
}
@@ -55,9 +85,17 @@ module.exports = function(state, emit) {
let content = '';
if (!state.fileInfo) {
state.fileInfo = createFileInfo(state);
- if (!state.fileInfo.nonce) {
+ if (downloadMetadata.status === 404) {
return notFound(state);
}
+ if (!state.fileInfo.nonce) {
+ // coming from something like the browser back button
+ return location.reload();
+ }
+ }
+
+ if (state.fileInfo.dead) {
+ return notFound(state);
}
if (!state.transfer && !state.fileInfo.requiresPassword) {
@@ -83,7 +121,7 @@ module.exports = function(state, emit) {
${state.modal && modal(state, emit)}
diff --git a/app/ui/downloadCompleted.js b/app/ui/downloadCompleted.js
index c357c5f11..22fd7ba44 100644
--- a/app/ui/downloadCompleted.js
+++ b/app/ui/downloadCompleted.js
@@ -2,6 +2,7 @@ const html = require('choo/html');
const assets = require('../../common/assets');
module.exports = function(state) {
+ const btnText = state.user.loggedIn ? 'okButton' : 'sendYourFilesLink';
return html`
${state.translate('downloadFinish')}
-
-
+
+
${state.translate('trySendDescription')}
${state.translate('sendYourFilesLink')}${state.translate(btnText)}
+
+ ${state.translate('reportFile')}
+
`;
};
diff --git a/app/ui/downloadDialog.js b/app/ui/downloadDialog.js
new file mode 100644
index 000000000..cd0722228
--- /dev/null
+++ b/app/ui/downloadDialog.js
@@ -0,0 +1,58 @@
+const html = require('choo/html');
+
+module.exports = function() {
+ return function(state, emit, close) {
+ const archive = state.fileInfo;
+ return html`
+
+
+ ${state.translate('downloadConfirmTitle')}
+
+
+ ${state.translate('downloadConfirmDescription')}
+
+
+
+
+
+
+ ${state.translate('reportFile')}
+
+ `;
+
+ function toggleDownloadEnabled(event) {
+ event.stopPropagation();
+ const checked = event.target.checked;
+ const btn = document.getElementById('download-btn');
+ btn.disabled = !checked;
+ }
+
+ function download(event) {
+ event.preventDefault();
+ close();
+ event.target.disabled = true;
+ emit('download', archive);
+ }
+ };
+};
diff --git a/app/ui/downloadPassword.js b/app/ui/downloadPassword.js
index 9357310b3..86c98fd9b 100644
--- a/app/ui/downloadPassword.js
+++ b/app/ui/downloadPassword.js
@@ -21,6 +21,12 @@ module.exports = function(state, emit) {
onsubmit="${checkPassword}"
data-no-csrf
>
+
${state.modal && modal(state, emit)}
@@ -13,12 +14,17 @@ module.exports = function(state, emit) {
${state.translate('errorPageHeader')}
-
+
${state.translate('trySendDescription')}
${state.translate('sendYourFilesLink')}${state.translate(btnText)}
diff --git a/app/ui/footer.js b/app/ui/footer.js
index d86555e8b..18aa0cbed 100644
--- a/app/ui/footer.js
+++ b/app/ui/footer.js
@@ -1,7 +1,5 @@
const html = require('choo/html');
const Component = require('choo/component');
-const version = require('../../package.json').version;
-const { browserName } = require('../utils');
class Footer extends Component {
constructor(name, state) {
@@ -15,12 +13,36 @@ class Footer extends Component {
createElement() {
const translate = this.state.translate;
- const browser = browserName();
- const feedbackUrl = `https://qsurvey.mozilla.com/s3/Firefox-Send-Product-Feedback?ver=${version}&browser=${browser}`;
return html`
`;
}
diff --git a/app/ui/header.js b/app/ui/header.js
index 4960c7a53..afc9fa64b 100644
--- a/app/ui/header.js
+++ b/app/ui/header.js
@@ -26,7 +26,17 @@ class Header extends Component {
`
: html`
+<<<<<<< HEAD
+=======
+
+
+>>>>>>> 11319080a8fe012cc6bde61b4ad4ccdec3c2e618
`;
return html`
diff --git a/app/ui/home.js b/app/ui/home.js
index cfa385649..aa12bedc1 100644
--- a/app/ui/home.js
+++ b/app/ui/home.js
@@ -5,6 +5,9 @@ const modal = require('./modal');
const intro = require('./intro');
module.exports = function(state, emit) {
+ if (state.user.loginRequired && !state.user.loggedIn) {
+ emit('signup-cta', 'required');
+ }
const archives = state.storage.files
.filter(archive => !archive.expired)
.map(archive => archiveTile(state, emit, archive));
diff --git a/app/ui/legal.js b/app/ui/legal.js
index 41763226b..1f4aad6e5 100644
--- a/app/ui/legal.js
+++ b/app/ui/legal.js
@@ -2,6 +2,7 @@ const html = require('choo/html');
const modal = require('./modal');
module.exports = function(state, emit) {
+ state.modal = null;
return html`
${state.modal && modal(state, emit)}
diff --git a/app/ui/modal.js b/app/ui/modal.js
index 8fa851110..3636af8a9 100644
--- a/app/ui/modal.js
+++ b/app/ui/modal.js
@@ -6,7 +6,7 @@ module.exports = function(state, emit) {
class="absolute inset-0 flex items-center justify-center overflow-hidden z-40 bg-white md:rounded-xl md:my-8 dark:bg-grey-90"
>
${state.modal(state, emit, close)}
diff --git a/app/ui/noStreams.js b/app/ui/noStreams.js
index d31ad7772..52cb7d052 100644
--- a/app/ui/noStreams.js
+++ b/app/ui/noStreams.js
@@ -19,9 +19,9 @@ module.exports = function(state, emit) {
diff --git a/app/ui/report.js b/app/ui/report.js
new file mode 100644
index 000000000..1c2ae3d77
--- /dev/null
+++ b/app/ui/report.js
@@ -0,0 +1,139 @@
+const html = require('choo/html');
+const raw = require('choo/html/raw');
+const assets = require('../../common/assets');
+
+const REPORTABLES = ['Malware', 'Pii', 'Abuse'];
+
+module.exports = function(state, emit) {
+ let submitting = false;
+ const file = state.fileInfo;
+ if (!file) {
+ return html`
+
+
+
+ ${state.translate('reportUnknownDescription')}
+
+
+ ${raw(
+ replaceLinks(state.translate('reportReasonCopyright'), [
+ 'https://www.mozilla.org/about/legal/report-infringement/'
+ ])
+ )}
+
+
+
+ `;
+ }
+ if (file.reported) {
+ return html`
+
+
+
+ `;
+ }
+ return html`
+
+
+
+
+ ${state.translate('reportFile')}
+
+
+ ${state.translate('reportDescription')}
+
+
+
+
+
+ `;
+
+ function optionChanged(event) {
+ event.stopPropagation();
+ const button = event.currentTarget.nextElementSibling;
+ button.disabled = false;
+ }
+
+ function report(event) {
+ event.stopPropagation();
+ event.preventDefault();
+ if (submitting) {
+ return;
+ }
+ submitting = true;
+ state.fileInfo.reported = true;
+ const form = event.target;
+ emit('report', { reason: form.reason.value });
+ }
+
+ function replaceLinks(str, urls) {
+ let i = 0;
+ const s = str.replace(
+ /
([^<]+)<\/a>/g,
+ (m, v) => `${v}`
+ );
+ return `
${s}
`;
+ }
+};
diff --git a/app/ui/shareDialog.js b/app/ui/shareDialog.js
index de6f6f9c2..a85633ecc 100644
--- a/app/ui/shareDialog.js
+++ b/app/ui/shareDialog.js
@@ -9,11 +9,9 @@ module.exports = function(name, url) {
${state.translate('notifyUploadEncryptDone')}
-
+
${state.translate('shareLinkDescription')}
- ${name}
+ ${name}
-
-
+
+
${state.translate('accountBenefitTitle')}
- ${state.translate('accountBenefitSync')}
- - ${state.translate('accountBenefitMoz')}
-
+
-
+ ${state.user.loginRequired
+ ? ''
+ : html`
+
+ `}
`;
diff --git a/app/user.js b/app/user.js
index c43039417..49d8decb6 100644
--- a/app/user.js
+++ b/app/user.js
@@ -76,6 +76,10 @@ export default class User {
return this.info.access_token;
}
+ get refreshToken() {
+ return this.info.refresh_token;
+ }
+
get maxSize() {
return this.loggedIn
? this.limits.MAX_FILE_SIZE
@@ -94,6 +98,10 @@ export default class User {
: this.limits.ANON.MAX_DOWNLOADS;
}
+ get loginRequired() {
+ return this.authConfig && this.authConfig.fxa_required;
+ }
+
async metricId() {
return this.loggedIn ? hashId(this.info.uid) : undefined;
}
@@ -135,6 +143,7 @@ export default class User {
const code_challenge = await preparePkce(this.storage);
const options = {
action: 'email',
+ access_type: 'offline',
client_id: this.authConfig.client_id,
code_challenge,
code_challenge_method: 'S256',
@@ -192,12 +201,69 @@ export default class User {
});
const userInfo = await infoResponse.json();
userInfo.access_token = auth.access_token;
+ userInfo.refresh_token = auth.refresh_token;
userInfo.fileListKey = await getFileListKey(this.storage, auth.keys_jwe);
this.info = userInfo;
this.storage.remove('pkceVerifier');
}
- logout() {
+ async refresh() {
+ if (!this.refreshToken) {
+ return false;
+ }
+ try {
+ const tokenResponse = await fetch(this.authConfig.token_endpoint, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({
+ client_id: this.authConfig.client_id,
+ grant_type: 'refresh_token',
+ refresh_token: this.refreshToken
+ })
+ });
+ if (tokenResponse.ok) {
+ const auth = await tokenResponse.json();
+ const info = { ...this.info, access_token: auth.access_token };
+ this.info = info;
+ return true;
+ }
+ } catch (e) {
+ console.error(e);
+ }
+ await this.logout();
+ return false;
+ }
+
+ async logout() {
+ try {
+ if (this.refreshToken) {
+ await fetch(this.authConfig.revocation_endpoint, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({
+ refresh_token: this.refreshToken
+ })
+ });
+ }
+ if (this.bearerToken) {
+ await fetch(this.authConfig.revocation_endpoint, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({
+ token: this.bearerToken
+ })
+ });
+ }
+ } catch (e) {
+ console.error(e);
+ // oh well, we tried
+ }
this.storage.clearLocalFiles();
this.info = {};
}
@@ -211,6 +277,14 @@ export default class User {
const key = b64ToArray(this.info.fileListKey);
const sha = await crypto.subtle.digest('SHA-256', key);
const kid = arrayToB64(new Uint8Array(sha)).substring(0, 16);
+ const retry = async () => {
+ const refreshed = await this.refresh();
+ if (refreshed) {
+ return await this.syncFileList();
+ } else {
+ return { incoming: true };
+ }
+ };
try {
const encrypted = await getFileList(this.bearerToken, kid);
const decrypted = await streamToArrayBuffer(
@@ -219,8 +293,7 @@ export default class User {
list = JSON.parse(textDecoder.decode(decrypted));
} catch (e) {
if (e.message === '401') {
- this.logout();
- return { incoming: true };
+ return retry(e);
}
}
changes = await this.storage.merge(list);
@@ -236,7 +309,9 @@ export default class User {
);
await setFileList(this.bearerToken, kid, encrypted);
} catch (e) {
- //
+ if (e.message === '401') {
+ return retry(e);
+ }
}
return changes;
}
diff --git a/app/utils.js b/app/utils.js
index 65a17262c..dd7fbf687 100644
--- a/app/utils.js
+++ b/app/utils.js
@@ -1,5 +1,10 @@
/* global Android */
-const html = require('choo/html');
+let html;
+try {
+ html = require('choo/html');
+} catch (e) {
+ // running in the service worker
+}
const b64 = require('base64-js');
function arrayToB64(array) {
@@ -137,12 +142,16 @@ function openLinksInNewTab(links, should = true) {
function browserName() {
try {
+ // order of these matters
if (/firefox/i.test(navigator.userAgent)) {
return 'firefox';
}
if (/edge/i.test(navigator.userAgent)) {
return 'edge';
}
+ if (/edg/i.test(navigator.userAgent)) {
+ return 'edgium';
+ }
if (/trident/i.test(navigator.userAgent)) {
return 'ie';
}
@@ -267,7 +276,15 @@ function setTranslate(t) {
translate = t;
}
+function concat(b1, b2) {
+ const result = new Uint8Array(b1.length + b2.length);
+ result.set(b1, 0);
+ result.set(b2, b1.length);
+ return result;
+}
+
module.exports = {
+ concat,
locale,
fadeOut,
delay,
diff --git a/app/zip.js b/app/zip.js
index 1363da828..bf62726f3 100644
--- a/app/zip.js
+++ b/app/zip.js
@@ -1,4 +1,4 @@
-import crc32 from 'crc/crc32';
+import crc32 from './crc32';
const encoder = new TextEncoder();
diff --git a/assets/wordmark.svg b/assets/wordmark.svg
index 58ed9db98..4f4b3689f 100644
--- a/assets/wordmark.svg
+++ b/assets/wordmark.svg
@@ -1,5 +1,5 @@