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)}
${content}
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 +======= + ${this.state.translate('title')} + + + +>>>>>>> 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) {
- + + +

${ archive.name @@ -55,6 +55,11 @@ module.exports = function(state, emit) { value="${state.translate('copyLinkButton')}" title="${state.translate('copyLinkButton')}" type="submit" /> +

+ ${state.translate('downloadConfirmDescription')} +

`; @@ -64,6 +69,7 @@ module.exports = function(state, emit) { const choice = event.target.value; const button = event.currentTarget.nextElementSibling; let title = button.title; + console.error(choice, title); switch (choice) { case 'copy': title = state.translate('copyLinkButton'); diff --git a/app/ui/notFound.js b/app/ui/notFound.js index f3cd4b9f1..ad70bb5d4 100644 --- a/app/ui/notFound.js +++ b/app/ui/notFound.js @@ -3,6 +3,7 @@ const assets = require('../../common/assets'); const modal = require('./modal'); module.exports = function(state, emit) { + const btnText = state.user.loggedIn ? 'okButton' : 'sendYourFilesLink'; return html`
${state.modal && modal(state, emit)} @@ -13,12 +14,22 @@ module.exports = function(state, emit) { ${state.translate('expiredTitle')} -

+

${state.translate('trySendDescription')}

${state.translate('sendYourFilesLink')}${state.translate(btnText)} +

+

+ ${state.translate('reportFile')}

diff --git a/app/ui/promo.js b/app/ui/promo.js index 659fcaf91..e514c2411 100644 --- a/app/ui/promo.js +++ b/app/ui/promo.js @@ -24,11 +24,11 @@ class Promo extends Component { alt="Firefox" /> - ${this.state.translate('trailheadPromo')}${' '} + ${`Like Firefox Send? You'll love our new full-device VPN. `} ${this.state.translate('learnMore')}${`Get it today`}
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` +
+
+

+ ${state.translate('reportedTitle')} +

+

+ ${state.translate('reportedDescription')} +

+ +

+ ${state.translate('okButton')} +

+
+
+ `; + } + return html` +
+
+
+

+ ${state.translate('reportFile')} +

+

+ ${state.translate('reportDescription')} +

+
+
+
    + ${REPORTABLES.map( + reportable => + html` +
  • + +
  • + ` + )} +
  • + ${raw( + replaceLinks(state.translate('reportReasonCopyright'), [ + 'https://www.mozilla.org/about/legal/report-infringement/' + ]) + )} +
  • +
+
+ +
+
+
+
+ `; + + 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 @@ -malware ose janë pjesë e një sulmi karremëzimi. +reportReasonPii = Këto kartela përmbajnë të dhëna personalisht të identifikueshme rreth meje. +reportReasonAbuse = Këto kartela përmbajnë lëndë të paligjshme ose abuzive. +reportReasonCopyright = Për të raportuar cenim të drejtash kopjimi ose shenjash tregtare, përdorni procesin e përshkruar në këtë faqe. +reportedTitle = Kartela të Raportuara +reportedDescription = Faleminderit. E kemimarrë raportin tuaj rreth këtyre kartelave. diff --git a/public/locales/sr/send.ftl b/public/locales/sr/send.ftl index 67ff0ab6d..8ab8e66a1 100644 --- a/public/locales/sr/send.ftl +++ b/public/locales/sr/send.ftl @@ -1,20 +1,19 @@ # Firefox Send is a brand name and should not be localized. title = Firefox Send -siteFeedback = Повратне информације importingFile = Увозим… encryptingFile = Шифрујем… decryptingFile = Дешифрујем… downloadCount = { $num -> - [one] преузимања - [few] преузимања - *[other] преузимања + [one] { $num } преузимања + [few] { $num } преузимања + *[other] { $num } преузимања } timespanHours = { $num -> - [one] сата - [few] сата - *[other] сати + [one] { $num } сата + [few] { $num } сата + *[other] { $num } сати } copiedUrl = Ископирано! unlockInputPlaceholder = Лозинка @@ -124,6 +123,7 @@ legalDateStamp = Издање 1.0, датум објављивања 12. мар # A short representation of a countdown timer containing the number of days, hours, and minutes remaining as digits, example "2d 11h 56m" expiresDaysHoursMinutes = { $days }д { $hours }ч { $minutes }м addFilesButton = Изаберите датотеке за отпремање +trustWarningMessage = Будите сигурни да верујете примаоцу пре дељења осетљивих података. uploadButton = Отпреми # the first part of the string 'Drag and drop files or click to send up to 1GB' dragAndDropFiles = Превуците и пустите датотеке @@ -162,3 +162,35 @@ shareLinkButton = Поделите везу shareMessage = Преузмите „{ $name }“ помоћу програма { -send-brand }: једноставно и безбедно дељење датотека trailheadPromo = Постоји начин да заштитите вашу приватност. Придружите се Firefox-у. learnMore = Сазнајте више. +downloadFlagged = Ова веза је онемогућена због кршења услова услуге. +downloadConfirmTitle = Још једна ствар +downloadConfirmDescription = Будите сигурни да верујете особи која вам је послала ову датотеку, јер не можемо обећати да неће оштетити ваш уређа. +# This string has a special case for '1' and [other] (default). If necessary for +# your language, you can add {$count} to your translations and use the +# standard CLDR forms, or only use the form for [other] if both strings should +# be identical. +downloadTrustCheckbox = + { $count -> + [one] Верујем особи која је послала ову датотеку + [few] Верујем особи која је послала ове датотеке + *[other] Верујем особама које су послале ове датотеке + } +# This string has a special case for '1' and [other] (default). If necessary for +# your language, you can add {$count} to your translations and use the +# standard CLDR forms, or only use the form for [other] if both strings should +# be identical. +reportFile = + { $count -> + [one] Пријави ову датотеку као сумњиву + [few] Пријави ове датотеке као сумњиве + *[other] Пријави ове датотеке као сумњиве + } +reportDescription = Помозите нам да схватимо шта се дешава. Шта мислите да није у реду са овим датотекама? +reportUnknownDescription = Идите на адресу везе коју желите да пријавите и изаберите “{ reportFile }”. +reportButton = Пријави +reportReasonMalware = Ове датотеке садрже злонамеран софтвер или су део напада за крађу идентитета. +reportReasonPii = Ове датотеке садрже моје личне податке. +reportReasonAbuse = Ове датотеке садрже илегални или насилни садржај. +reportReasonCopyright = Да бисте пријавили кршење ауторских права или заштитног знака, следите кораке на овој страници. +reportedTitle = Датотеке су пријављене +reportedDescription = Хвала вам. Примили смо вашу пријаву ових датотека. diff --git a/public/locales/su/send.ftl b/public/locales/su/send.ftl index ec06769ad..cd4ec9d33 100644 --- a/public/locales/su/send.ftl +++ b/public/locales/su/send.ftl @@ -1,6 +1,5 @@ # Firefox Send is a brand name and should not be localized. title = Firefox Send -siteFeedback = Pangdeudeul importingFile = Ngimpor... encryptingFile = Ngénkripsi... decryptingFile = Ngadékripsi... @@ -114,6 +113,7 @@ legalDateStamp = Versi 1.0, kaping 12 Maret 2019 # A short representation of a countdown timer containing the number of days, hours, and minutes remaining as digits, example "2d 11h 56m" expiresDaysHoursMinutes = { $days }p { $hours }j { $minutes }m addFilesButton = Pilih koropak unjalkeuneun +trustWarningMessage = Sing yakin yén anjeun percaya nalika ngabagi data sénsitip ka nu nampa. uploadButton = Unjal # the first part of the string 'Drag and drop files or click to send up to 1GB' dragAndDropFiles = Ésérkeun sarta ésotkeun koropak @@ -151,3 +151,31 @@ shareLinkButton = Bagikeun tutumbu shareMessage = Undeur "{ $name }" ku { -send-brand }: simpel, babagi koropak aman trailheadPromo = Aya cara pikeun ngamankeun privasi anjeun. Jabung jeung Firefox. learnMore = Lenyepan. +downloadFlagged = Ieu tutumbu ditumpurkeun alatan ngarumpak katangtuan layanan. +downloadConfirmTitle = Hiji deui +downloadConfirmDescription = Sing yakin yén anjeun percaya ka jalma nu ngirim ieu berkas kusabab kami teu bisa mariksa kaamanan ieu berkas. +# This string has a special case for '1' and [other] (default). If necessary for +# your language, you can add {$count} to your translations and use the +# standard CLDR forms, or only use the form for [other] if both strings should +# be identical. +downloadTrustCheckbox = + { $count -> + *[other] Kami percaya ka jalma nu ngirim ieu berkas + } +# This string has a special case for '1' and [other] (default). If necessary for +# your language, you can add {$count} to your translations and use the +# standard CLDR forms, or only use the form for [other] if both strings should +# be identical. +reportFile = + { $count -> + *[other] Laporkeun ieu berkas salaku picurigaeun + } +reportDescription = Béjakeun ka kami masalahna. Naon anu sakirana salah dina ieu berkas? +reportUnknownDescription = Mangga buka url tutumbu anu rék dilaporkeun sarta klik “{ reportFile }”. +reportButton = Laporkeun +reportReasonMalware = Ieu berkas ngandung malwér atawa bagian ti tarajang pising. +reportReasonPii = Ieu berkas ngandung émbaran pribadi kami. +reportReasonAbuse = Ieu berkas ngandung kontén ilégal atawa panyalahgunaan. +reportReasonCopyright = Pikeun ngalaporkeun rumpakan hak cipta atawa mérk dagang, paké prosés anu diécéskeun di dieu. +reportedTitle = Berkas Dilaporkeun +reportedDescription = Nuhun. Laporan anjeun ngeunaan ieu berkas geus katampa. diff --git a/public/locales/sv-SE/send.ftl b/public/locales/sv-SE/send.ftl index c9a6a4224..31fff5558 100644 --- a/public/locales/sv-SE/send.ftl +++ b/public/locales/sv-SE/send.ftl @@ -1,6 +1,5 @@ # Firefox Send is a brand name and should not be localized. title = Firefox Send -siteFeedback = Återkoppling importingFile = Importerar… encryptingFile = Krypterar… decryptingFile = Avkodar… @@ -116,6 +115,7 @@ legalDateStamp = Version 1.0, daterad den 12 mars 2019 # A short representation of a countdown timer containing the number of days, hours, and minutes remaining as digits, example "2d 11h 56m" expiresDaysHoursMinutes = { $days }d { $hours }t { $minutes }m addFilesButton = Välj filer som ska laddas upp +trustWarningMessage = Se till att du litar på din mottagare när du delar känslig information. uploadButton = Ladda upp # the first part of the string 'Drag and drop files or click to send up to 1GB' dragAndDropFiles = Dra och släpp filer @@ -153,3 +153,33 @@ shareLinkButton = Dela länk shareMessage = Ladda ner "{ $name }" med { -send-brand }: enkel, säker fildelning trailheadPromo = Det finns ett sätt att skydda din integritet. Gå med i Firefox. learnMore = Läs mer. +downloadFlagged = Den här länken har inaktiverats pga brott mot användarvillkoren. +downloadConfirmTitle = En sak till +downloadConfirmDescription = Se till att du litar på personen som skickade dig den här filen eftersom vi inte kan verifiera att den inte skadar din enhet. +# This string has a special case for '1' and [other] (default). If necessary for +# your language, you can add {$count} to your translations and use the +# standard CLDR forms, or only use the form for [other] if both strings should +# be identical. +downloadTrustCheckbox = + { $count -> + [one] Jag litar på personen som skickade denna filen + *[other] Jag litar på personen som skickade dessa filer + } +# This string has a special case for '1' and [other] (default). If necessary for +# your language, you can add {$count} to your translations and use the +# standard CLDR forms, or only use the form for [other] if both strings should +# be identical. +reportFile = + { $count -> + [one] Rapportera denna filen som misstänkt + *[other] Rapportera dessa filer som misstänkta + } +reportDescription = Hjälp oss att förstå vad som händer. Vad tycker du är fel med dessa filer? +reportUnknownDescription = Gå till den url till länken du vill rapportera och klicka på "{ reportFile }". +reportButton = Rapportera +reportReasonMalware = Dessa filer innehåller skadlig kod eller är en del av en nätfiskeattack. +reportReasonPii = Dessa filer innehåller personlig identifierbar information om mig. +reportReasonAbuse = Dessa filer innehåller olagligt eller våldsamt innehåll. +reportReasonCopyright = För att rapportera intrång i upphovsrätt eller varumärke, använd processen som beskrivs på den här sidan. +reportedTitle = Rapporterade filer +reportedDescription = Tack. Vi har fått din rapport om dessa filer. diff --git a/public/locales/te/send.ftl b/public/locales/te/send.ftl index bb8b61166..159ddbda5 100644 --- a/public/locales/te/send.ftl +++ b/public/locales/te/send.ftl @@ -1,6 +1,5 @@ # Firefox Send is a brand name and should not be localized. title = Firefox Send -siteFeedback = అభిప్రాయం importingFile = దిగుమతవుతోంది... encryptingFile = గుప్తీకరిస్తోంది... decryptingFile = వ్యక్తపరుస్తోంది... @@ -53,6 +52,9 @@ passwordSetError = ఈ సంకేతపదం పెట్టలేకపో -send-short-brand = పంపించు -firefox = Firefox -mozilla = Mozilla +notifyUploadEncryptDone = మీ ఫైలు గుప్తీకరించబడింది, పంపడానికి సిద్ధంగా ఉంది +# downloadCount is from the downloadCount string and timespan is a timespanMinutes string. ex. 'Expires after 2 downloads or 25 minutes' +archiveExpiryInfo = { $downloadCount } లేదా { $timespan } తర్వాత కాలంచెల్లుతుంది timespanMinutes = { $num -> [one] 1 నిమిషం @@ -111,7 +113,12 @@ accountBenefitTitle = ఒక { -firefox } ఖాతాని సృష్టి # $size is the size of the file, displayed using the fileSize message as format (e.g. "2.5MB") accountBenefitLargeFiles = { $size } పరిమాణం ఫైళ్ళ వరకు పంచుకోండి accountBenefitDownloadCount = ఫైళ్లను ఎక్కువ మందితో పంచుకోండి +accountBenefitTimeLimit = + { $count -> + *[other] లంకెలను { $count } రోజుల వరకు చేతనంగా ఉంచు + } accountBenefitSync = ఏదైనా పరికరం నుండి పంచుకున్న ఫైళ్ళను నిర్వహించండి +accountBenefitMoz = ఇతర { -mozilla } సేవల గురించి తెలుసుకోండి signOut = నిష్క్రమించు okButton = సరే downloadingTitle = దింపుకుంటోంది @@ -123,4 +130,7 @@ downloadFirefoxPromo = { -send-short-brand } క్రొత్త { -firefox } # the next line after the colon contains a file name shareLinkDescription = మీ ఫైలుకు లంకెను పంచుకోండి: shareLinkButton = లంకెను పంచుకోండి +# $name is the name of the file +shareMessage = “{ $name }”‌ని { -send-brand }తో దించుకోండి: తేలికైన, సురక్షితమైన ఫైలు పంచుకోలు సేవ +trailheadPromo = మీ అంతరంగికతను కాపాడుకోడానికి ఓ మార్గం ఉంది. Firefoxతో చేరండి. learnMore = ఇంకా తెలుసుకోండి. diff --git a/public/locales/th/send.ftl b/public/locales/th/send.ftl index 0f606d2f7..dea862153 100644 --- a/public/locales/th/send.ftl +++ b/public/locales/th/send.ftl @@ -1,6 +1,5 @@ # Firefox Send is a brand name and should not be localized. title = Firefox Send -siteFeedback = ข้อคิดเห็น importingFile = กำลังนำเข้า… encryptingFile = กำลังเข้ารหัส… decryptingFile = กำลังถอดรหัส… @@ -108,6 +107,7 @@ legalDateStamp = รุ่น 1.0 วันที่ 12 มีนาคม 2019 # A short representation of a countdown timer containing the number of days, hours, and minutes remaining as digits, example "2d 11h 56m" expiresDaysHoursMinutes = { $days } วัน { $hours } ชม. { $minutes } นาที addFilesButton = เลือกไฟล์ที่จะอัปโหลด +trustWarningMessage = ตรวจสอบให้แน่ใจว่าคุณเชื่อใจผู้รับของคุณขณะที่คุณแบ่งปันข้อมูลที่ละเอียดอ่อน uploadButton = อัปโหลด # the first part of the string 'Drag and drop files or click to send up to 1GB' dragAndDropFiles = ลากแล้วปล่อยไฟล์ @@ -144,3 +144,31 @@ shareLinkButton = แบ่งปันลิงก์ shareMessage = ดาวน์โหลด “{ $name }” ด้วย { -send-brand }: การแบ่งปันไฟล์ที่ง่ายและเป็นส่วนตัว trailheadPromo = มีวิธีปกป้องความเป็นส่วนตัวของคุณ เข้าร่วม Firefox learnMore = เรียนรู้เพิ่มเติม +downloadFlagged = ลิงก์นี้ถูกปิดการใช้งานเนื่องจากละเมิดข้อกำหนดในการให้บริการ +downloadConfirmTitle = อีกหนึ่งอย่าง +downloadConfirmDescription = ตรวจสอบให้แน่ใจว่าคุณเชื่อถือคนที่ส่งไฟล์นี้ให้คุณ เพราะเราไม่สามารถยืนยันได้ว่าไฟล์นี้จะไม่เป็นอันตรายต่ออุปกรณ์ของคุณ +# This string has a special case for '1' and [other] (default). If necessary for +# your language, you can add {$count} to your translations and use the +# standard CLDR forms, or only use the form for [other] if both strings should +# be identical. +downloadTrustCheckbox = + { $count -> + *[other] ฉันเชื่อใจคนที่ส่งไฟล์เหล่านี้ + } +# This string has a special case for '1' and [other] (default). If necessary for +# your language, you can add {$count} to your translations and use the +# standard CLDR forms, or only use the form for [other] if both strings should +# be identical. +reportFile = + { $count -> + *[other] รายงานไฟล์เหล่านี้ว่าน่าสงสัย + } +reportDescription = ช่วยให้เราเข้าใจสิ่งที่เกิดขึ้น คุณคิดอย่างไรว่าไฟล์เหล่านี้ผิดปกติ? +reportUnknownDescription = โปรดไปที่ URL ของลิงก์ที่คุณต้องการรายงานและคลิก “{ reportFile }” +reportButton = รายงาน +reportReasonMalware = ไฟล์เหล่านี้มีมัลแวร์หรือเป็นส่วนหนึ่งของการโจมตีแบบฟิชชิ่ง +reportReasonPii = ไฟล์เหล่านี้มีข้อมูลที่สามารถระบุตัวบุคคลได้เกี่ยวกับฉัน +reportReasonAbuse = ไฟล์เหล่านี้มีเนื้อหาที่ผิดกฎหมายหรือไม่เหมาะสม +reportReasonCopyright = หากต้องการรายงานการละเมิดลิขสิทธิ์หรือเครื่องหมายการค้าให้ใช้กระบวนการที่อธิบายไว้ใน หน้านี้ +reportedTitle = ไฟล์ถูกรายงานแล้ว +reportedDescription = ขอบคุณ เราได้รับรายงานของคุณเกี่ยวกับไฟล์เหล่านี้แล้ว diff --git a/public/locales/tl/send.ftl b/public/locales/tl/send.ftl index 6e674df4d..6e5de3fea 100644 --- a/public/locales/tl/send.ftl +++ b/public/locales/tl/send.ftl @@ -1,120 +1,118 @@ # Firefox Send is a brand name and should not be localized. -title = Firefox Ipadala -siteSubtitle = eksperimento sa web +title = Firefox Send siteFeedback = Feedback -uploadPageHeader = Pribadong, Naka-encrypt na Pagbabahagi ng File -uploadPageExplainer = Magpadala ng mga file sa pamamagitan ng isang ligtas, pribado, at naka-encrypt na link na awtomatikong mawawalan ng bisa upang matiyak na ang iyong mga bagay-bagay ay hindi mananatiling online magpakailanman. -uploadPageLearnMore = Matuto ng higit pa -uploadPageDropMessage = I-drop ang iyong file dito upang simulan ang pag-upload -uploadPageSizeMessage = Para sa pinaka maaasahang operasyon, pinakamahusay na panatilihin ang iyong file sa ilalim ng 1GB -uploadPageBrowseButton = Pumili ng isang file sa iyong computer -uploadPageBrowseButton1 = Pumili ng isang file na mai-upload -uploadPageMultipleFilesAlert = Kasalukuyang hindi sinusuportahan ang pag-upload ng maramihang mga file o isang folder. -uploadPageBrowseButtonTitle = I-upload ang file -uploadingPageProgress = Uploading { $filename } ({ $size }) importingFile = Importing… -verifyingFile = Pinatutunayan... encryptingFile = Encrypting… decryptingFile = Decrypting… -notifyUploadDone = Natapos na ang iyong pag-upload. -uploadingPageMessage = Sa sandaling mag-upload ang iyong file, makakapagtakda ka ng mga expire na pagpipilian. -uploadingPageCancel = Kanselahin ang pag-upload -uploadCancelNotification = Kinansela ang iyong pag-upload. -uploadingPageLargeFileMessage = Ang file na ito ay malaki at maaaring tumagal ng ilang sandali upang mag-upload. Umupo nang masikip! -uploadingFileNotification = Abisuhan ako kapag nakumpleto na ang pag-upload. -uploadSuccessConfirmHeader = Handa nang Ipadala -uploadSvgAlt = I-upload -uploadSuccessTimingHeader = Mag-e-expire ang link sa iyong file pagkatapos ng 1 pag-download o sa loob ng 24 na oras. -expireInfo = Mag-e-expire ang link sa iyong file pagkatapos ng { $downloadCount } o { $timespan }. downloadCount = { $num -> [one] 1 pag-download *[other] { $num } na mga pag-download } -timespanHours = - { $num -> - *[one] 1 oras - } -copyUrlFormLabelWithName = Kopyahin at ibahagi ang link upang ipadala ang iyong file: { $filename } -copyUrlFormButton = Kopyahin sa clipboard copiedUrl = Naikopya! -deleteFileButton = Burahin ang file -sendAnotherFileLink = Magpadala ng isang file -# Alternative text used on the download link/button (indicates an action). -downloadAltText = I-download -downloadsFileList = Mga Pag-download -# Used as header in a column indicating the amount of time left before a -# download link expires (e.g. "10h 5m") -timeFileList = Oras -# Used as header in a column indicating the number of times a file has been -# downloaded -downloadFileName = I-download { $filename } -downloadFileSize = ({ $size }) -unlockInputLabel = Ilagay ang Password unlockInputPlaceholder = Password unlockButtonLabel = I-unlock -downloadFileTitle = I-download ang Na-encrypt na File -# Firefox Send is a brand name and should not be localized. -downloadMessage = Ang iyong kaibigan ay nagpapadala sa iyo ng isang file na may Firefox Send, isang serbisyo na nagbibigay-daan sa iyo upang magbahagi ng mga file sa isang ligtas, pribado, at naka-encrypt na link na awtomatikong mawawalan ng bisa upang matiyak na ang iyong mga bagay-bagay ay hindi mananatiling online magpakailanman. -# Text and title used on the download link/button (indicates an action). downloadButtonLabel = I-download -downloadNotification = Nakumpleto na ang iyong pag-download. downloadFinish = Kumpleto ang Download -# This message is displayed when uploading or downloading a file, e.g. "(1,3 MB of 10 MB)". fileSizeProgress = ({ $partialSize } ng { $totalSize }) -# Firefox Send is a brand name and should not be localized. sendYourFilesLink = Subukan ang Firefox Ipadala -downloadingPageProgress = Downloading { $filename } ({ $size }) -downloadingPageMessage = Paki-iwan ang tab na ito habang binuksan namin ang iyong file at i-decrypt ito. -errorAltText = Mag-upload ng error errorPageHeader = May nagkamali! -errorPageMessage = Nagkaroon ng error sa pag-upload ng file. -errorPageLink = Magpadala ng isang file fileTooBig = Ang file na iyon ay masyadong malaki upang mag-upload. Dapat itong mas mababa sa { $size }. linkExpiredAlt = Nag-expire na ang link -expiredPageHeader = Nag-expire na ang link na ito o hindi kailanman umiiral sa unang lugar! notSupportedHeader = Ang iyong browser ay hindi suportado. -# Firefox Send is a brand name and should not be localized. -notSupportedDetail = Sa kasamaang palad hindi sinusuportahan ng browser na ito ang teknolohiya sa web na nagpapagana ng Firefox Send. Kailangan mong subukan ang ibang browser. Inirerekomenda namin ang Firefox! notSupportedLink = Bakit hindi suportado ang aking browser? notSupportedOutdatedDetail = Sa kasamaang palad ang bersyon na ito ng Firefox ay hindi sumusuporta sa teknolohiya ng web na nagpapagana ng Firefox Send. Kailangan mong i-update ang iyong browser. updateFirefox = I-update ang Firefox -downloadFirefoxButtonSub = Libreng Download -uploadedFile = File -copyFileList = Kopyahin ang URL -# expiryFileList is used as a column header -expiryFileList = Magtatapos Sa -deleteFileList = I-delete -nevermindButton = Hindi bale -legalHeader = Mga Tuntunin at Pagkapribado -legalNoticeTestPilot = Ang Firefox Ipadala ay kasalukuyang eksperimentong Test Pilot, at napapailalim sa Mga Tuntunin ng Serbisyo at Paunawa sa Privacy. Maaari kang matuto nang higit pa tungkol sa eksperimentong ito at ang koleksyon ng data nito dito. -legalNoticeMozilla = Ang paggamit ng website ng Ipadala ang Firefox ay napapailalim din sa Mga Patakaran sa Privacy ng Website ng Mozilla at Mga Tuntunin ng Paggamit ng Website. -deletePopupText = Tanggalin ang file na ito? -deletePopupYes = Oo deletePopupCancel = Kanselahin deleteButtonHover = I-delete -copyUrlHover = Kopyahin ang URL footerLinkLegal = Legal -# Test Pilot is a proper name and should not be localized. -footerLinkAbout = Tungkol sa Test Pilot footerLinkPrivacy = Privacy -footerLinkTerms = Mga term footerLinkCookies = Mga cookie -requirePasswordCheckbox = Mangailangan ng isang password upang i-download ang file na ito -addPasswordButton = Magdagdag ng password -changePasswordButton = Palitan passwordTryAgain = Maling password. Subukan muli. -reportIPInfringement = Report IP Infringement -javascriptRequired = Nangangailangan ang JavaScript sa JavaScript -whyJavascript = Bakit ang JavaScript ay nangangailangan ng JavaScript? +javascriptRequired = Nangangailangan ang Firefox Send ng JavaScript +whyJavascript = Bakit ang Firefox Send ay nangangailangan ng JavaScript? enableJavascript = Mangyaring paganahin ang JavaScript at subukan muli. # A short representation of a countdown timer containing the number of hours and minutes remaining as digits, example "13h 47m" expiresHoursMinutes = { $hours }h { $minutes }m # A short representation of a countdown timer containing the number of minutes remaining as digits, example "56m" expiresMinutes = { $minutes }m -# A short status message shown when a password is successfully set -passwordIsSet = I-set ang password # A short status message shown when the user enters a long password maxPasswordLength = Pinakamataas na haba ng password: { $length } # A short status message shown when there was an error setting the password passwordSetError = Hindi maitakda ang password na ito + +## Send version 2 strings + +# Firefox Send, Send, Firefox, Mozilla are proper names and should not be localized +-send-brand = Firefox send +-send-short-brand = I-send +-firefox = Firefox +-mozilla = Mozilla +introTitle = Simple, pribadong pagbabahagi ng file +notifyUploadEncryptDone = Ang iyong file ay naka-encrypt at handa na i-send +# downloadCount is from the downloadCount string and timespan is a timespanMinutes string. ex. 'Expires after 2 downloads or 25 minutes' +archiveExpiryInfo = mag-e-expire pagkatapos { $downloadCount } o { $timespan } +timespanMinutes = + { $num -> + [one] 1 minuto + *[other] { $num } mga minuto + } +timespanDays = + { $num -> + [one] 1 araw + *[other] { $num } mga araw + } +timespanWeeks = + { $num -> + [one] 1 linggo + *[other] { $num } mga linggo + } +fileCount = + { $num -> + [one] 1 file + *[other] { $num } mga file + } +# byte abbreviation +bytes = B +# kibibyte abbreviation +kb = KB +# mebibyte abbreviation +mb = MB +# gibibyte abbreviation +gb = GB +# localized number and byte abbreviation. example "2.5MB" +fileSize = { $num }{ $units } +# $size is the size of the file, displayed using the fileSize message as format (e.g. "2.5MB") +totalSize = Kabuuang sukat: { $size } +# the next line after the colon contains a file name +copyLinkDescription = Kopyahin ang link upang ibahagi ang iyong file: +copyLinkButton = Kopyahin ang link +downloadTitle = I-download ang mga file +expiredTitle = Ang link na ito ay nag-expire. +downloadFirefox = I-download { -firefox } +legalTitle = { -send-short-brand } Abiso sa Privacy +legalDateStamp = Bersyon 1.0, petsa ng Marso 12, 2019 +# A short representation of a countdown timer containing the number of days, hours, and minutes remaining as digits, example "2d 11h 56m" +expiresDaysHoursMinutes = { $days }d { $hours }h { $minutes }m +addFilesButton = Piliin ang mga file na mai-upload +uploadButton = I-upload +# the first part of the string 'Drag and drop files or click to send up to 1GB' +dragAndDropFiles = I-drag at i-drop ang mga file +addPassword = Protektahan gamit ang password +emailPlaceholder = Ipasok ang iyong email +# $size is the size of the file, displayed using the fileSize message as format (e.g. "2.5MB") +signInSizeBump = Mag-sign in upang magpadala ng hanggang sa { $size } +signInOnlyButton = Mag sign-in +# $size is the size of the file, displayed using the fileSize message as format (e.g. "2.5MB") +accountBenefitLargeFiles = Ibahagi ang mga file hanggang sa { $size } +accountBenefitDownloadCount = Ibahagi ang mga file sa ibang tao +accountBenefitMoz = Alamin ang tungkol sa iba pang mga serbisyo ng { -mozilla } +signOut = Mag sign-out +okButton = OK +downloadingTitle = Pag-download +noStreamsWarning = Maaaring hindi mai-decrypt ng browser na ito ang isang file na malaki. +noStreamsOptionCopy = Kopyahin ang link upang buksan sa isa pang browser +noStreamsOptionFirefox = Subukan ang aming paboritong browser +noStreamsOptionDownload = Magpatuloy sa browser na ito +shareLinkButton = Ibahagi ang link +learnMore = Matuto ng higit pa. diff --git a/public/locales/tr/send.ftl b/public/locales/tr/send.ftl index 3891eb74c..9365e9a6c 100644 --- a/public/locales/tr/send.ftl +++ b/public/locales/tr/send.ftl @@ -1,6 +1,5 @@ # Firefox Send is a brand name and should not be localized. title = Firefox Send -siteFeedback = Görüş bildir importingFile = İçe aktarılıyor… encryptingFile = Şifreleniyor… decryptingFile = Şifre çözülüyor… @@ -105,13 +104,14 @@ tooManyArchives = *[other] En fazla { $count } arşive izin veriliyor. } expiredTitle = Bu bağlantının süresi doldu. -notSupportedDescription = { -send-brand } bu tarayıcıyı desteklemiyor. { -send-short-brand } en iyi şekilde { -firefox }’un son sürümüyle ve çoğu tarayıcının güncel sürümüyla çalışır. +notSupportedDescription = { -send-brand } bu tarayıcıyı desteklemiyor. { -send-short-brand } en iyi şekilde { -firefox }’un son sürümüyle ve çoğu tarayıcının güncel sürümüyle çalışır. downloadFirefox = { -firefox }’u indir legalTitle = { -send-short-brand } Gizlilik Bildirimi legalDateStamp = Sürüm 1.0, 12 Mart 2019 # A short representation of a countdown timer containing the number of days, hours, and minutes remaining as digits, example "2d 11h 56m" expiresDaysHoursMinutes = { $days } g { $hours } sa { $minutes } dk addFilesButton = Yüklenecek dosyaları seçin +trustWarningMessage = Hassas verileri paylaşırken alıcıya güvendiğinizden emin olun. uploadButton = Yükle # the first part of the string 'Drag and drop files or click to send up to 1GB' dragAndDropFiles = Dosyaları sürükleyip bırakarak @@ -149,3 +149,33 @@ shareLinkButton = Bağlantıyı paylaş shareMessage = “{ $name }” dosyasını { -send-brand } ile indirin: basit ve güvenli dosya paylaşımı trailheadPromo = Gizliliğinizi korumanın bir yolu var. Firefox’a katılın. learnMore = Daha fazla bilgi alın. +downloadFlagged = Bu bağlantı hizmet koşullarımızı ihlal ettiği için devre dışı bırakıldı. +downloadConfirmTitle = Bir şey daha +downloadConfirmDescription = Bu dosyayı gönderen kişiye güvendiğinizden emin olun. Dosyanın cihazınıza zarar vermeyeceğini garanti edemeyiz. +# This string has a special case for '1' and [other] (default). If necessary for +# your language, you can add {$count} to your translations and use the +# standard CLDR forms, or only use the form for [other] if both strings should +# be identical. +downloadTrustCheckbox = + { $count -> + [one] Bu dosyayı gönderen kişiye güveniyorum + *[other] Bu dosyaları gönderen kişiye güveniyorum + } +# This string has a special case for '1' and [other] (default). If necessary for +# your language, you can add {$count} to your translations and use the +# standard CLDR forms, or only use the form for [other] if both strings should +# be identical. +reportFile = + { $count -> + [one] Bu dosyanın şüpheli olduğunu bildir + *[other] Bu dosyaların şüpheli olduğunu bildir + } +reportDescription = Meseleyi anlamamıza yardımcı olun. Bu dosyalardaki sorun nedir? +reportUnknownDescription = Lütfen rapor etmek istediğiniz bağlantının adresine girip “{ reportFile }” bağlantısına tıklayın. +reportButton = Şikâyet et +reportReasonMalware = Bu dosyalar kötü amaçlı yazılım içeriyor veya kimlik avı saldırında kullanılıyor. +reportReasonPii = Bu dosyalar benim hakkımda kişisel bilgiler içeriyor. +reportReasonAbuse = Bu dosyalar yasa dışı veya istismar amaçlı içerik içeriyor. +reportReasonCopyright = Telif hakkı veya ticari marka ihlallerini bildirmek için bu sayfadaki adımları izlemelisiniz. +reportedTitle = Dosyalar rapor edildi +reportedDescription = Teşekkür ederiz. Bu dosyalarla ilgili şikâyetinizi aldık. diff --git a/public/locales/trs/send.ftl b/public/locales/trs/send.ftl new file mode 100644 index 000000000..20161642f --- /dev/null +++ b/public/locales/trs/send.ftl @@ -0,0 +1,106 @@ +# Firefox Send is a brand name and should not be localized. +title = Firefox Send +importingFile = Hìaj a'nïn huan'ānj… +encryptingFile = Nagi'iaj hùij… +decryptingFile = Hìaj nâ'nïn… +downloadCount = + { $num -> + [one] 1 sa nadunin + *[other] { $num } nej sa nadunin + } +timespanHours = + { $num -> + [one] 1 ôra + *[other] { $num } nej ôra + } +copiedUrl = Ngà gisîj guxunj! +unlockInputPlaceholder = Da'nga' huìi +unlockButtonLabel = Na'nïn riñanj +downloadButtonLabel = Nadunïnj +downloadFinish = Ngà nahui nanïnj +fileSizeProgress = ({ $partialSize } guendâ { $totalSize }) +sendYourFilesLink = Garahuè dàj 'iaj sun Firefox Send +errorPageHeader = Huā sa gahui a'nan'! +fileTooBig = Ûta yachìj hua archibô dan. Da'ui gā li doj ga da' { $size } +linkExpiredAlt = Nitāj si ni'ñānj lînk gà' +notSupportedHeader = Nitāj si huā hue'ê riña sa nana'uî't. +notSupportedLink = Nù huin saj nitāj si huā hue'ê riña sa nana'uí? +notSupportedOutdatedDetail = Nu unùkuaj Firefox nan gi'iaj sunj ngà sa 'iaj sun ngà Firefox Send. Da'uît nāgi'iaj nakàt riña sa nana'uî't han. +updateFirefox = Nagi'iaj nakà Firefox +deletePopupCancel = Duyichin' +deleteButtonHover = Dure' +footerLinkLegal = Nuguan' a'nï'ïn +footerLinkPrivacy = Sa hùii +footerLinkCookies = Nej kôki +passwordTryAgain = Sê da'nga' huì dan huin. Ginù huin ñû. +javascriptRequired = Ni'ñānj Firefox Send JavaScript +whyJavascript = Nù huin saj ni'ñānj Firefox Send JavaScript rà'aj? +enableJavascript = Gi'iaj sunūj u ga'nïn gi'iaj sun JavaScript nī yakāj da'nga' ñû. +# A short representation of a countdown timer containing the number of hours and minutes remaining as digits, example "13h 47m" +expiresHoursMinutes = { $hours }h { $minutes }m +# A short representation of a countdown timer containing the number of minutes remaining as digits, example "56m" +expiresMinutes = { $minutes }m +# A short status message shown when the user enters a long password +maxPasswordLength = Dānaj gā yachìj da'nga huìi: { $length } +# A short status message shown when there was an error setting the password +passwordSetError = Na'ue gārayinaj da'nga huìi + +## Send version 2 strings + +# Firefox Send, Send, Firefox, Mozilla are proper names and should not be localized +-send-brand = Firefox Send +-send-short-brand = Send +-firefox = Firefox +-mozilla = Mozilla +introTitle = Hìo nī huì ga’ue duyingâ’t archîbo +introDescription = { -send-brand } a’nïn duyingâ’t archîbo ngà ‘ngō da’nga’rán hia nī ngà ‘ngō lînk nare’ man‘an. Dànanj nī ‘ngō rïnt ni’in sa duyingâ’t nī si lînk si ginu yitïn riña lînia. +notifyUploadEncryptDone = Ngà huā ran si archibôt nī ngà huā yugui da’ ga’nïnjt gan’an +# downloadCount is from the downloadCount string and timespan is a timespanMinutes string. ex. 'Expires after 2 downloads or 25 minutes' +archiveExpiryInfo = Narè’ man ne’ rukù { $downloadCount } asi { $timespan } +timespanMinutes = + { $num -> + [one] 1 minûtu + *[other] { $num } minûtu + } +timespanDays = + { $num -> + [one] 1 gui + *[other] { $num } gui + } +timespanWeeks = + { $num -> + [one] 1 semâna + *[other] { $num } semâna + } +fileCount = + { $num -> + [one] 1 archîbo + *[other] { $num } archîbo + } +# byte abbreviation +bytes = B +# kibibyte abbreviation +kb = KB +# mebibyte abbreviation +mb = MB +# gibibyte abbreviation +gb = GB +# localized number and byte abbreviation. example "2.5MB" +fileSize = { $num }{ $units } +# $size is the size of the file, displayed using the fileSize message as format (e.g. "2.5MB") +totalSize = Dàj nìko yàchi: { $size } +# the next line after the colon contains a file name +copyLinkDescription = Guxūn lînk da' ga'ue duyingâ't archibô: +copyLinkButton = Guxûn lînk +downloadTitle = Nadunïnj nej archîbo +downloadFirefox = Nadunïnj { -firefox } +legalTitle = Nuguan huì nikāj { -send-short-brand } +uploadButton = Nādusîj +addPassword = Dugumî da’nga’ huìi man +emailPlaceholder = Gāchrūn si korreot +signInOnlyButton = Gāyi'ì sēsiûn +signOut = Narun' sesiôn +okButton = Ga'ue +downloadingTitle = Hìaj nadunīnj man +shareLinkButton = Duguachîn enlâse +learnMore = Gāhuin chrūn doj. diff --git a/public/locales/uk/send.ftl b/public/locales/uk/send.ftl index 9fb4979e9..54a5e128e 100644 --- a/public/locales/uk/send.ftl +++ b/public/locales/uk/send.ftl @@ -1,6 +1,5 @@ # Firefox Send is a brand name and should not be localized. title = Firefox Send -siteFeedback = Відгуки importingFile = Імпортуємо... encryptingFile = Шифруємо... decryptingFile = Розшифровуємо... @@ -124,6 +123,7 @@ legalDateStamp = Версія 1.0 від 12 березня 2019 року # A short representation of a countdown timer containing the number of days, hours, and minutes remaining as digits, example "2d 11h 56m" expiresDaysHoursMinutes = { $days }д { $hours }г { $minutes }хв addFilesButton = Оберіть файли для вивантаження +trustWarningMessage = Переконайтеся, що довіряєте одержувачу коли ділитеся вразливими даними. uploadButton = Вивантажити # the first part of the string 'Drag and drop files or click to send up to 1GB' dragAndDropFiles = Перетягуйте файли @@ -162,3 +162,35 @@ shareLinkButton = Поділитись посиланням shareMessage = Завантажте “{ $name }” з { -send-brand }: простий та безпечний обмін файлами trailheadPromo = Існує спосіб захистити вашу приватність. Приєднуйтесь до Firefox. learnMore = Докладніше. +downloadFlagged = Це посилання вимкнено через порушення умов надання послуг. +downloadConfirmTitle = Ще порада +downloadConfirmDescription = Переконайтеся, що довіряєте відправнику цього файлу, оскільки ми не можемо перевірити, чи він не зашкодить вашому пристрою. +# This string has a special case for '1' and [other] (default). If necessary for +# your language, you can add {$count} to your translations and use the +# standard CLDR forms, or only use the form for [other] if both strings should +# be identical. +downloadTrustCheckbox = + { $count -> + [one] Я довіряю відправнику цього файлу + [few] Я довіряю відправнику цих файлів + *[many] Я довіряю відправнику цих файлів + } +# This string has a special case for '1' and [other] (default). If necessary for +# your language, you can add {$count} to your translations and use the +# standard CLDR forms, or only use the form for [other] if both strings should +# be identical. +reportFile = + { $count -> + [one] Повідомити, що цей файл є підозрілим + [few] Повідомити, що ці файли є підозрілими + *[many] Повідомити, що ці файли є підозрілими + } +reportDescription = Допоможіть нам зрозуміти, що відбувається. Що, на вашу думку, з цими файлами не так? +reportUnknownDescription = Перейдіть до url-адреси посилання, про яке хочете надіслати звіт, та натисніть “{ reportFile }”. +reportButton = Надіслати звіт +reportReasonMalware = Ці файли містять зловмисне програмне забезпечення або є частиною фішинг-атаки. +reportReasonPii = Ці файли містять мої особисті дані. +reportReasonAbuse = Ці файли містять незаконний або образливий вміст. +reportReasonCopyright = Щоб повідомити про порушення авторських прав або торговельних марок, скористайтеся настановами з цієї сторінки. +reportedTitle = Звіт про файли надіслано +reportedDescription = Дякуємо. Ми отримали ваш звіт про ці файли. diff --git a/public/locales/vi/send.ftl b/public/locales/vi/send.ftl index 197130cad..1d9fb020c 100644 --- a/public/locales/vi/send.ftl +++ b/public/locales/vi/send.ftl @@ -1,6 +1,5 @@ # Firefox Send is a brand name and should not be localized. title = Firefox Send -siteFeedback = Phản hồi importingFile = Đang nhập... encryptingFile = Đang mã hóa... decryptingFile = Đang giải mã... @@ -108,6 +107,7 @@ legalDateStamp = Phiên bản 1.0, ngày 12 tháng 3 năm 2019 # A short representation of a countdown timer containing the number of days, hours, and minutes remaining as digits, example "2d 11h 56m" expiresDaysHoursMinutes = { $days } ngày { $hours } giờ { $minutes } phút addFilesButton = Chọn tập tin để tải lên +trustWarningMessage = Hãy chắc chắn rằng bạn tin tưởng người nhận khi chia sẻ dữ liệu nhạy cảm. uploadButton = Tải lên # the first part of the string 'Drag and drop files or click to send up to 1GB' dragAndDropFiles = Kéo và thả tập tin @@ -144,3 +144,31 @@ shareLinkButton = Chia sẻ liên kết shareMessage = Tải xuống “{ $name }“ với { -send-brand }: chia sẻ tập tin đơn giản, an toàn trailheadPromo = Đây là một cách để bảo vệ sự riêng tư của bạn. Tham gia Firefox. learnMore = Tìm hiểu thêm. +downloadFlagged = Liên kết này đã bị vô hiệu hóa do vi phạm các điều khoản dịch vụ. +downloadConfirmTitle = Một điều nữa +downloadConfirmDescription = Hãy chắc chắn rằng bạn tin tưởng người đã gửi cho bạn tập tin này vì chúng tôi không thể xác minh rằng nó sẽ không gây hại cho thiết bị của bạn. +# This string has a special case for '1' and [other] (default). If necessary for +# your language, you can add {$count} to your translations and use the +# standard CLDR forms, or only use the form for [other] if both strings should +# be identical. +downloadTrustCheckbox = + { $count -> + *[other] Tôi tin tưởng người đã gửi những tập tin này + } +# This string has a special case for '1' and [other] (default). If necessary for +# your language, you can add {$count} to your translations and use the +# standard CLDR forms, or only use the form for [other] if both strings should +# be identical. +reportFile = + { $count -> + *[other] Báo cáo các tập tin này là đáng ngờ + } +reportDescription = Hãy giúp chúng tôi hiểu những gì đang diễn ra. Bạn nghĩ gì có gì không đúng với những tập tin này? +reportUnknownDescription = Vui lòng truy cập url của liên kết bạn muốn báo cáo và nhấp vào “{ reportFile }”. +reportButton = Báo cáo +reportReasonMalware = Những tập tin này chứa phần mềm độc hại hoặc là một phần của cuộc tấn công lừa đảo. +reportReasonPii = Những tập tin này chứa thông tin cá nhân về tôi. +reportReasonAbuse = Những tập tin này chứa nội dung bất hợp pháp hoặc lạm dụng. +reportReasonCopyright = Để báo cáo vi phạm bản quyền hoặc nhãn hiệu, hãy sử dụng quy trình được mô tả tại trang này. +reportedTitle = Đã báo cáo tập tin +reportedDescription = Cảm ơn bạn. Chúng tôi đã nhận được báo cáo của bạn về các tập tin này. diff --git a/public/locales/yo/send.ftl b/public/locales/yo/send.ftl new file mode 100644 index 000000000..a066d111b --- /dev/null +++ b/public/locales/yo/send.ftl @@ -0,0 +1,118 @@ +# Firefox Send is a brand name and should not be localized. +title = Firefox Send je oruko ile-ise kan, kò sì ye ki a so di ibile. +importingFile = akowọle… +encryptingFile = Fifi ọrọ ṣiṣẹ… +decryptingFile = Ti nkọ nkan… +downloadCount = + { $num -> + [one] ìsíwá kan… + *[other] ọ̀pọ̀ ìsíwá… + } +timespanHours = + { $num -> + [one] Wákàtí kan + *[other] Ọ̀pọ wákàtí + } +copiedUrl = dakọ +unlockInputPlaceholder = + aṣínà + ọ̀rọ̀-aṣínà + ọ̀rọ̀-agbaniwọlé +unlockButtonLabel = ṣí +downloadButtonLabel = Ìgbasílẹ̀ +downloadFinish = + Ìsíwá parí + Ìgbasílẹ̀ parí +sendYourFilesLink = + Gbìyànjúu Firefox Send + Gbìyànjú lo Firefox Send + Dán Firefox Send wò +errorPageHeader = Nnkan o lo daadaa! +fileTooBig = + Fáìlì yìí tóbijù láti gbà sókè. Ó ní láti kéré sí { $size } + Fáìlì yìí tóbijù láti gbà sókè. Ó ní láti kéré ju { $size } lọ +linkExpiredAlt = + Ojú-òpó ti kásẹ̀ + Ojú-òpó ti pajújé + Ọ̀nà-òpó ti kásẹ̀ + Ọ̀nà-òpó ti pajújé +notSupportedHeader = + Èrọ-ìfarakọ́ra rẹ ò ní ìbátan + Ojú-òpó ìfarakọ́ra rẹ ò ní ìbátan +notSupportedLink = + Kí ló ṣe tí ẹ̀rọ-ìfarakọ́ra mi ò ní ìbátan? + Kí ló ṣe tí ẹ̀rọ-aṣàwárí mi ò ní ìbátan? + Kí nìdí tí ẹ̀rọ-ìfarakọ́ra mi ò ní ìbátan? + Kí nìdí tí ẹ̀rọ-aṣàwárí mi ò ní ìbátan? +notSupportedOutdatedDetail = Ó ṣe, wípé ẹ̀dà Firefox yí ò ní àtìlẹyìn ẹ̀rọ-alátagbà tí ó ń mú Firefox Send ṣiṣẹ́. O ní láti ṣe àgbéga èdà ẹ̀rọ-aṣàwárí rẹ kó bágbàmu. +updateFirefox = Mú Firefox bágbàmu +deletePopupCancel = + Nù kúrò + Parẹ́ +deleteButtonHover = + Mú kúrò + Parẹ́ +footerLinkLegal = + b’ófin mu + n’ílànà òfin +footerLinkPrivacy = + Ibi ìkọ̀kọ̀ + Ibi ìpamọ́ +footerLinkCookies = + Cookie + Àmì-ẹ̀rọ aránṣẹ́-jíṣẹ́ +passwordTryAgain = + Ọ̀rọ̀-aṣínà kò tọ́. Gbìyànjú síi + Ọ̀rọ̀-aṣíde kò tọ́. Gbìyànjú síi +javascriptRequired = Firefox Send nílòo JavaScript +whyJavascript = + Kí nìdí tí Firefox fi nílòo JavaScript? + Kí nìdí tí Firefox ṣe nílòo JavaScript? +enableJavascript = + Jọ̀wọ́ tán JavaScript sílẹ̀ kí o sì gbìyànjú si. + Jọ̀wọ́ ṣí JavaScript sílẹ̀ kí o sì gbìyànjú si. +# A short representation of a countdown timer containing the number of hours and minutes remaining as digits, example "13h 47m" +expiresHoursMinutes = + { $hours }w { $minutes }i + { $hours }wákàtí { $minutes }iṣẹ́jú +# A short representation of a countdown timer containing the number of minutes remaining as digits, example "56m" +expiresMinutes = { $minutes }i +# A short status message shown when the user enters a long password +maxPasswordLength = Ìdíwọ̀n ọ̀rọ̀-aṣínà: { $length } +# A short status message shown when there was an error setting the password +passwordSetError = + Ọ̀rọ̀-aṣínà yí kò ṣeé gbé kalẹ̀ + Ọ̀rọ̀-aṣínà yí kò leè fẹsẹ̀ múlẹ̀ + +## Send version 2 strings + +# Firefox Send, Send, Firefox, Mozilla are proper names and should not be localized +-send-brand = Firefox Send +-send-short-brand = + Fi ránṣẹ́ + Firánṣẹ́ +-firefox = Firefox +-mozilla = Mozilla +introTitle = + Fáìlì pípín níkọ̀kọ̀ tó dẹrùn + Fáìlì pípín níkọ̀kọ̀ onírọ̀rùn +# byte abbreviation +bytes = B +# kibibyte abbreviation +kb = Kilobaiti +# mebibyte abbreviation +mb = Megabaiti +# gibibyte abbreviation +gb = Gigabaiti +downloadTitle = Se igabisile faili +addFilesButton = E yan awon faili lati gbasoke +# the first part of the string 'Drag and drop files or click to send up to 1GB' +dragAndDropFiles = E mu awon faili ki ede ju si bi +emailPlaceholder = E fi imeli si +accountBenefitDownloadCount = E pin faili pelu awon eyan si +okButton = O DA +downloadingTitle = N se igabsile +noStreamsOptionFirefox = E gbiyanju asawakiri to je ayanfe wa +noStreamsOptionDownload = Tesiwaju pelu aṣàwákiri yi +trailheadPromo = Ona wa lati dabobo ipamo re. Darapo mo Firefox +learnMore = Keeko si diff --git a/public/locales/yua/send.ftl b/public/locales/yua/send.ftl new file mode 100644 index 000000000..be2e48dc2 --- /dev/null +++ b/public/locales/yua/send.ftl @@ -0,0 +1,20 @@ +# Firefox Send is a brand name and should not be localized. +title = Firefox Send +# A short representation of a countdown timer containing the number of minutes remaining as digits, example "56m" +expiresMinutes = { $minutes }m + +## Send version 2 strings + +-send-short-brand = Send +-firefox = Firefox +-mozilla = Mozilla +# byte abbreviation +bytes = B +# kibibyte abbreviation +kb = KB +# mebibyte abbreviation +mb = MB +# gibibyte abbreviation +gb = GB +# localized number and byte abbreviation. example "2.5MB" +fileSize = { $num }{ $units } diff --git a/public/locales/zh-CN/send.ftl b/public/locales/zh-CN/send.ftl index bfc473df4..dce0b236c 100644 --- a/public/locales/zh-CN/send.ftl +++ b/public/locales/zh-CN/send.ftl @@ -1,6 +1,5 @@ # Firefox Send is a brand name and should not be localized. title = Firefox Send -siteFeedback = 反馈 importingFile = 正在导入… encryptingFile = 正在加密… decryptingFile = 正在解密… @@ -103,7 +102,7 @@ tooManyFiles = # count will always be > 10 tooManyArchives = { $count -> - *[other] 只可上传 { $count } 个档案。 + *[other] 只可上传 { $count } 个压缩文件。 } expiredTitle = 此链接已过期。 notSupportedDescription = { -send-brand } 无法在此浏览器上正常工作。{ -send-short-brand } 与最新版本 { -firefox } 配合使用体验最佳,也适用于目前的大多数浏览器。 @@ -113,6 +112,7 @@ legalDateStamp = 版本 1.0,于 2019年3月12日 # A short representation of a countdown timer containing the number of days, hours, and minutes remaining as digits, example "2d 11h 56m" expiresDaysHoursMinutes = { $days } 天 { $hours } 小时 { $minutes } 分钟 addFilesButton = 选择要上传的文件 +trustWarningMessage = 分享敏感数据时,请确保您信任接收人。 uploadButton = 上传 # the first part of the string 'Drag and drop files or click to send up to 1GB' dragAndDropFiles = 拖放文件 @@ -148,5 +148,35 @@ shareLinkDescription = 您的文件链接: shareLinkButton = 分享链接 # $name is the name of the file shareMessage = 使用 { -send-brand } 下载“{ $name }”:简单、安全的文件分享服务 -trailheadPromo = 有种方法可以保护您的隐私,加入 Firefox。 +trailheadPromo = 捍卫隐私不是幻想。加入 Firefox 一同抗争。 learnMore = 详细了解。 +downloadFlagged = 由于违反服务条款,此链接已被禁用。 +downloadConfirmTitle = 除此之外 +downloadConfirmDescription = 请确保您信任发送此文件的人,因为我们无法验证该文件是否会损坏您的设备。 +# This string has a special case for '1' and [other] (default). If necessary for +# your language, you can add {$count} to your translations and use the +# standard CLDR forms, or only use the form for [other] if both strings should +# be identical. +downloadTrustCheckbox = + { $count -> + [one] 我信任发送此文件的人 + *[other] 我信任发送这些文件的人 + } +# This string has a special case for '1' and [other] (default). If necessary for +# your language, you can add {$count} to your translations and use the +# standard CLDR forms, or only use the form for [other] if both strings should +# be identical. +reportFile = + { $count -> + [one] 举报此可疑文件 + *[other] 举报这些可疑文件 + } +reportDescription = 帮助我们了解发生了什么。您认为这些文件存在什么问题? +reportUnknownDescription = 请转至您要举报的链接 URL,然后点击 “{ reportFile }”。 +reportButton = 举报 +reportReasonMalware = 这些文件包含恶意软件或是网络钓鱼攻击的一环。 +reportReasonPii = 这些文件包含我的个人身份信息。 +reportReasonAbuse = 这些文件包含非法或滥用内容。 +reportReasonCopyright = 要举报版权或商标侵权,请按照此页面中所述步骤。 +reportedTitle = 文件已举报 +reportedDescription = 感谢,我们已收到您关于这些文件的举报。 diff --git a/public/locales/zh-TW/send.ftl b/public/locales/zh-TW/send.ftl index 21308d09e..8ac60fe0a 100644 --- a/public/locales/zh-TW/send.ftl +++ b/public/locales/zh-TW/send.ftl @@ -1,6 +1,5 @@ # Firefox Send is a brand name and should not be localized. title = Firefox Send -siteFeedback = 意見回饋 importingFile = 匯入中… encryptingFile = 加密中… decryptingFile = 解密中… @@ -108,6 +107,7 @@ legalDateStamp = 1.0 版,2019 年 3 月 12 日生效 # A short representation of a countdown timer containing the number of days, hours, and minutes remaining as digits, example "2d 11h 56m" expiresDaysHoursMinutes = { $days } 天 { $hours } 小時 { $minutes } 分鐘 addFilesButton = 選擇要上傳的檔案 +trustWarningMessage = 分享敏感資料時,請務必確認收件者是可信任的人。 uploadButton = 上傳 # the first part of the string 'Drag and drop files or click to send up to 1GB' dragAndDropFiles = 拖放檔案到此處 @@ -144,3 +144,31 @@ shareLinkButton = 分享鏈結 shareMessage = 使用 { -send-brand } 下載「{ $name }」: 簡單安全的檔案分享機制 trailheadPromo = 有種方法可以保護您的隱私,加入 Firefox。 learnMore = 了解更多。 +downloadFlagged = 由於違反了服務條款,已停用此鏈結。 +downloadConfirmTitle = 還有一件事 +downloadConfirmDescription = 因為我們無法檢查此檔案是否會傷害您的裝置,請務必確認發送者是否可受信任。 +# This string has a special case for '1' and [other] (default). If necessary for +# your language, you can add {$count} to your translations and use the +# standard CLDR forms, or only use the form for [other] if both strings should +# be identical. +downloadTrustCheckbox = + { $count -> + *[other] 我信任傳送檔案給我的人 + } +# This string has a special case for '1' and [other] (default). If necessary for +# your language, you can add {$count} to your translations and use the +# standard CLDR forms, or only use the form for [other] if both strings should +# be identical. +reportFile = + { $count -> + *[other] 回報檔案為可疑檔案 + } +reportDescription = 請幫助我們釐清發生了什麼事。您覺得這些檔案有什麼問題? +reportUnknownDescription = 請到想要回報的鏈結網址點擊「{ reportFile }」。 +reportButton = 回報 +reportReasonMalware = 這些檔案包含惡意軟體,或是釣魚攻擊的一部分。 +reportReasonPii = 這些檔案包含我的個人資訊。 +reportReasonAbuse = 這些檔案包含非法或濫用內容。 +reportReasonCopyright = 若檔案內容侵犯了著作權或商標,請根據此頁面當中描述的方式進行回報。 +reportedTitle = 已回報檔案問題 +reportedDescription = 感謝您。我們已經收到您對這些檔案的問題回報。 diff --git a/server/amplitude.js b/server/amplitude.js index a5bbb3556..e818e5ee1 100644 --- a/server/amplitude.js +++ b/server/amplitude.js @@ -1,5 +1,8 @@ const crypto = require('crypto'); +<<<<<<< HEAD //const geoip = require('fxa-geodb')(); +======= +>>>>>>> 11319080a8fe012cc6bde61b4ad4ccdec3c2e618 const fetch = require('node-fetch'); const config = require('./config'); const pkg = require('../package.json'); @@ -21,20 +24,11 @@ function userId(fileId, ownerId) { return hash.digest('hex').substring(32); } -function location(ip) { - try { - return geoip(ip); - } catch (e) { - return {}; - } -} - function statUploadEvent(data) { - const loc = location(data.ip); const event = { session_id: -1, - country: loc.country, - region: loc.state, + country: data.country, + region: data.state, user_id: userId(data.id, data.owner), app_version: pkg.version, time: truncateToHour(Date.now()), @@ -54,11 +48,10 @@ function statUploadEvent(data) { } function statDownloadEvent(data) { - const loc = location(data.ip); const event = { session_id: -1, - country: loc.country, - region: loc.state, + country: data.country, + region: data.state, user_id: userId(data.id, data.owner), app_version: pkg.version, time: truncateToHour(Date.now()), @@ -74,11 +67,10 @@ function statDownloadEvent(data) { } function statDeleteEvent(data) { - const loc = location(data.ip); const event = { session_id: -1, - country: loc.country, - region: loc.state, + country: data.country, + region: data.state, user_id: userId(data.id, data.owner), app_version: pkg.version, time: truncateToHour(Date.now()), @@ -93,8 +85,37 @@ function statDeleteEvent(data) { return sendBatch([event]); } -function clientEvent(event, ua, language, session_id, deltaT, platform, ip) { - const loc = location(ip); +function statReportEvent(data) { + const event = { + session_id: -1, + country: data.country, + region: data.state, + user_id: userId(data.id, data.owner), + app_version: pkg.version, + time: truncateToHour(Date.now()), + event_type: 'server_report', + event_properties: { + reason: data.reason, + agent: data.agent, + download_limit: data.dlimit, + download_count: data.download_count, + ttl: data.ttl + }, + event_id: data.download_count + 1 + }; + return sendBatch([event]); +} + +function clientEvent( + event, + ua, + language, + session_id, + deltaT, + platform, + country, + state +) { const ep = event.event_properties || {}; const up = event.user_properties || {}; const event_properties = { @@ -130,7 +151,7 @@ function clientEvent(event, ua, language, session_id, deltaT, platform, ip) { }; return { app_version: pkg.version, - country: loc.country, + country: country, device_id: event.device_id, event_properties, event_type: event.event_type, @@ -138,7 +159,7 @@ function clientEvent(event, ua, language, session_id, deltaT, platform, ip) { os_name: ua.os.name, os_version: ua.os.version, platform, - region: loc.state, + region: state, session_id, time: event.time + deltaT, user_id: event.user_id, @@ -170,6 +191,7 @@ module.exports = { statUploadEvent, statDownloadEvent, statDeleteEvent, + statReportEvent, clientEvent, sendBatch }; diff --git a/server/bin/dev.js b/server/bin/dev.js index f1a1dec1f..f5c7ce956 100644 --- a/server/bin/dev.js +++ b/server/bin/dev.js @@ -14,7 +14,7 @@ module.exports = function(app, devServer) { expressWs(wsapp, null, { perMessageDeflate: false }); routes(wsapp); wsapp.ws('/api/ws', require('../routes/ws')); - wsapp.listen(8081, config.listen_address); + wsapp.listen(1338, config.listen_address); assets.setMiddleware(devServer.middleware); app.use(morgan('dev', { stream: process.stderr })); diff --git a/server/config.js b/server/config.js index 55cefc62c..cf469ca37 100644 --- a/server/config.js +++ b/server/config.js @@ -9,6 +9,16 @@ const conf = convict({ default: '', env: 'S3_BUCKET' }, + s3_endpoint: { + format: String, + default: '', + env: 'S3_ENDPOINT' + }, + s3_use_path_style_endpoint: { + format: Boolean, + default: false, + env: 'S3_USE_PATH_STYLE_ENDPOINT' + }, gcs_bucket: { format: String, default: '', @@ -61,7 +71,7 @@ const conf = convict({ }, redis_host: { format: String, - default: 'localhost', + default: 'mock', env: 'REDIS_HOST' }, redis_event_expire: { @@ -69,6 +79,16 @@ const conf = convict({ default: false, env: 'REDIS_EVENT_EXPIRE' }, + redis_retry_time: { + format: Number, + default: 10000, + env: 'REDIS_RETRY_TIME' + }, + redis_retry_delay: { + format: Number, + default: 500, + env: 'REDIS_RETRY_DELAY' + }, listen_address: { format: 'ipaddress', default: '0.0.0.0', @@ -100,6 +120,11 @@ const conf = convict({ default: '', env: 'SENTRY_DSN' }, + sentry_host: { + format: String, + default: 'https://sentry.prod.mozaws.net', + env: 'SENTRY_HOST' + }, env: { format: ['production', 'development', 'test'], default: 'development', @@ -130,9 +155,14 @@ const conf = convict({ default: `${tmpdir()}${path.sep}send-${randomBytes(4).toString('hex')}`, env: 'FILE_DIR' }, + fxa_required: { + format: Boolean, + default: true, + env: 'FXA_REQUIRED' + }, fxa_url: { format: 'url', - default: 'https://send-fxa.dev.lcip.org', + default: 'http://localhost:3030', env: 'FXA_URL' }, fxa_client_id: { @@ -145,10 +175,35 @@ const conf = convict({ default: 'https://identity.mozilla.com/apps/send', env: 'FXA_KEY_SCOPE' }, + fxa_csp_oauth_url: { + format: String, + default: '', + env: 'FXA_CSP_OAUTH_URL' + }, + fxa_csp_content_url: { + format: String, + default: '', + env: 'FXA_CSP_CONTENT_URL' + }, + fxa_csp_profile_url: { + format: String, + default: '', + env: 'FXA_CSP_PROFILE_URL' + }, + fxa_csp_profileimage_url: { + format: String, + default: '', + env: 'FXA_CSP_PROFILEIMAGE_URL' + }, survey_url: { format: String, default: '', env: 'SURVEY_URL' + }, + ip_db: { + format: String, + default: '', + env: 'IP_DB' } }); diff --git a/server/keychain.js b/server/keychain.js new file mode 100644 index 000000000..e7dc0156d --- /dev/null +++ b/server/keychain.js @@ -0,0 +1,53 @@ +const { Crypto } = require('@peculiar/webcrypto'); +const crypto = new Crypto(); + +const encoder = new TextEncoder(); +const decoder = new TextDecoder(); + +module.exports = class Keychain { + constructor(secretKeyB64) { + if (secretKeyB64) { + this.rawSecret = new Uint8Array(Buffer.from(secretKeyB64, 'base64')); + } else { + throw new Error('key is required'); + } + this.secretKeyPromise = crypto.subtle.importKey( + 'raw', + this.rawSecret, + 'HKDF', + false, + ['deriveKey'] + ); + this.metaKeyPromise = this.secretKeyPromise.then(function(secretKey) { + return crypto.subtle.deriveKey( + { + name: 'HKDF', + salt: new Uint8Array(), + info: encoder.encode('metadata'), + hash: 'SHA-256' + }, + secretKey, + { + name: 'AES-GCM', + length: 128 + }, + false, + ['decrypt'] + ); + }); + } + + async decryptMetadata(ciphertext) { + const metaKey = await this.metaKeyPromise; + const plaintext = await crypto.subtle.decrypt( + { + name: 'AES-GCM', + iv: new Uint8Array(12), + tagLength: 128 + }, + metaKey, + ciphertext + ); + return JSON.parse(decoder.decode(plaintext)); + } +}; diff --git a/server/layout.js b/server/layout.js index b616a06fa..5f815a54e 100644 --- a/server/layout.js +++ b/server/layout.js @@ -7,7 +7,7 @@ module.exports = function(state, body = '') { - Factual SendPass + Foursquare SendPass @@ -15,8 +15,8 @@ module.exports = function(state, body = '') { - - + + diff --git a/server/metadata.js b/server/metadata.js index 1c5993169..a0ca9e10e 100644 --- a/server/metadata.js +++ b/server/metadata.js @@ -1,12 +1,45 @@ +const crypto = require('crypto'); + +function makeToken(secret, counter) { + const hmac = crypto.createHmac('sha256', secret); + hmac.update(String(counter)); + return hmac.digest('hex'); +} + class Metadata { - constructor(obj) { + constructor(obj, storage) { + this.id = obj.id; this.dl = +obj.dl || 0; + this.dlToken = +obj.dlToken || 0; this.dlimit = +obj.dlimit || 1; - this.pwd = String(obj.pwd) === 'true'; + this.pwd = !!+obj.pwd; this.owner = obj.owner; this.metadata = obj.metadata; this.auth = obj.auth; this.nonce = obj.nonce; + this.flagged = !!obj.flagged; + this.dead = !!obj.dead; + this.fxa = !!+obj.fxa; + this.storage = storage; + } + + async getDownloadToken() { + if (this.dlToken >= this.dlimit) { + throw new Error('limit'); + } + this.dlToken = await this.storage.incrementField(this.id, 'dlToken'); + // another request could have also incremented + if (this.dlToken > this.dlimit) { + throw new Error('limit'); + } + return makeToken(this.owner, this.dlToken); + } + + async verifyDownloadToken(token) { + const validTokens = Array.from({ length: this.dlToken }, (_, i) => + makeToken(this.owner, i + 1) + ); + return validTokens.includes(token); } } diff --git a/server/middleware/auth.js b/server/middleware/auth.js index 133b09922..b1af98565 100644 --- a/server/middleware/auth.js +++ b/server/middleware/auth.js @@ -46,7 +46,7 @@ module.exports = { if (id && ownerToken) { try { req.meta = await storage.metadata(id); - if (!req.meta) { + if (!req.meta || req.meta.dead) { return res.sendStatus(404); } const metaOwner = Buffer.from(req.meta.owner, 'utf8'); @@ -70,6 +70,27 @@ module.exports = { const token = authHeader.split(' ')[1]; req.user = await fxa.verify(token); } - return next(); + if (req.user) { + next(); + } else { + res.sendStatus(401); + } + }, + dlToken: async function(req, res, next) { + const authHeader = req.header('Authorization'); + if (authHeader && /^Bearer\s/i.test(authHeader)) { + const token = authHeader.split(' ')[1]; + const id = req.params.id; + req.meta = await storage.metadata(id); + if (!req.meta || req.meta.dead) { + return res.sendStatus(404); + } + req.authorized = await req.meta.verifyDownloadToken(token); + } + if (req.authorized) { + next(); + } else { + res.sendStatus(401); + } } }; diff --git a/server/routes/delete.js b/server/routes/delete.js index c0b70bd12..586cd9dc1 100644 --- a/server/routes/delete.js +++ b/server/routes/delete.js @@ -6,11 +6,13 @@ module.exports = async function(req, res) { const id = req.params.id; const meta = req.meta; const ttl = await storage.ttl(id); - await storage.del(id); + await storage.kill(id); res.sendStatus(200); statDeleteEvent({ id, ip: req.ip, + country: req.geo.country, + state: req.geo.state, owner: meta.owner, download_count: meta.dl, ttl, diff --git a/server/routes/done.js b/server/routes/done.js new file mode 100644 index 000000000..8c61a084f --- /dev/null +++ b/server/routes/done.js @@ -0,0 +1,31 @@ +const storage = require('../storage'); +const { statDownloadEvent } = require('../amplitude'); + +module.exports = async function(req, res) { + try { + const id = req.params.id; + const meta = req.meta; + const ttl = await storage.ttl(id); + statDownloadEvent({ + id, + ip: req.ip, + owner: meta.owner, + download_count: meta.dl, + ttl, + agent: req.ua.browser.name || req.ua.ua.substring(0, 6) + }); + await storage.incrementField(id, 'dl'); + if (meta.dl + 1 >= meta.dlimit) { + // Only dlimit number of tokens will be issued + // after which /download/token will return 403 + // however the protocol doesn't prevent one token + // from making all the downloads and assumes + // clients are well behaved. If this becomes + // a problem we can keep track of used tokens. + await storage.kill(id); + } + res.sendStatus(200); + } catch (e) { + res.sendStatus(404); + } +}; diff --git a/server/routes/download.js b/server/routes/download.js index acf2253cf..17fcb7a8e 100644 --- a/server/routes/download.js +++ b/server/routes/download.js @@ -1,46 +1,14 @@ const storage = require('../storage'); -const mozlog = require('../log'); -const log = mozlog('send.download'); -const { statDownloadEvent } = require('../amplitude'); module.exports = async function(req, res) { const id = req.params.id; try { - const meta = req.meta; - const fileStream = await storage.get(id); - let cancelled = false; - - req.on('close', () => { - cancelled = true; - fileStream.destroy(); - }); - - fileStream.pipe(res).on('finish', async () => { - if (cancelled) { - return; - } - - const dl = meta.dl + 1; - const dlimit = meta.dlimit; - const ttl = await storage.ttl(id); - statDownloadEvent({ - id, - ip: req.ip, - owner: meta.owner, - download_count: dl, - ttl, - agent: req.ua.browser.name || req.ua.ua.substring(0, 6) - }); - try { - if (dl >= dlimit) { - await storage.del(id); - } else { - await storage.incrementField(id, 'dl'); - } - } catch (e) { - log.info('StorageError:', id); - } + const { length, stream } = await storage.get(id); + res.writeHead(200, { + 'Content-Type': 'application/octet-stream', + 'Content-Length': length }); + stream.pipe(res); } catch (e) { res.sendStatus(404); } diff --git a/server/routes/exists.js b/server/routes/exists.js index da49c0193..5f4fdeee1 100644 --- a/server/routes/exists.js +++ b/server/routes/exists.js @@ -3,6 +3,9 @@ const storage = require('../storage'); module.exports = async (req, res) => { try { const meta = await storage.metadata(req.params.id); + if (!meta || meta.dead) { + return res.sendStatus(404); + } res.set('WWW-Authenticate', `send-v1 ${meta.nonce}`); res.send({ requiresPassword: meta.pwd diff --git a/server/routes/filelist.js b/server/routes/filelist.js index 700fe7452..eb000b311 100644 --- a/server/routes/filelist.js +++ b/server/routes/filelist.js @@ -13,28 +13,21 @@ function id(user, kid) { module.exports = { async get(req, res) { - if (!req.user) { - return res.sendStatus(401); - } const kid = req.params.id; try { const fileId = id(req.user, kid); - const contentLength = await storage.length(fileId); - const fileStream = await storage.get(fileId); + const { length, stream } = await storage.get(fileId); res.writeHead(200, { 'Content-Type': 'application/octet-stream', - 'Content-Length': contentLength + 'Content-Length': length }); - fileStream.pipe(res); + stream.pipe(res); } catch (e) { res.sendStatus(404); } }, async post(req, res) { - if (!req.user) { - return res.sendStatus(401); - } const kid = req.params.id; try { const limiter = new Limiter(1024 * 1024 * 10); diff --git a/server/routes/index.js b/server/routes/index.js index 057598154..bb22be02d 100644 --- a/server/routes/index.js +++ b/server/routes/index.js @@ -31,40 +31,60 @@ module.exports = function(app) { next(); }); if (!IS_DEV) { - app.use( - helmet.contentSecurityPolicy({ - directives: { - defaultSrc: ["'self'"], - connectSrc: [ - "'self'", - 'wss://*.dev.lcip.org', - 'wss://*.send.nonprod.cloudops.mozgcp.net', - 'wss://send.firefox.com', - 'https://*.dev.lcip.org', - 'https://accounts.firefox.com', - 'https://*.accounts.firefox.com', - 'https://sentry.prod.mozaws.net' - ], - imgSrc: [ - "'self'", - 'https://*.dev.lcip.org', - 'https://firefoxusercontent.com', - 'https://secure.gravatar.com' - ], - scriptSrc: [ - "'self'", - function(req) { - return `'nonce-${req.cspNonce}'`; - } - ], - formAction: ["'none'"], - frameAncestors: ["'none'"], - objectSrc: ["'none'"], - reportUri: '/__cspreport__' - } - }) - ); + let csp = { + directives: { + defaultSrc: ["'self'"], + connectSrc: [ + "'self'", + config.base_url.replace(/^https:\/\//, 'wss://') + ], + imgSrc: ["'self'"], + scriptSrc: [ + "'self'", + function(req) { + return `'nonce-${req.cspNonce}'`; + } + ], + formAction: ["'none'"], + frameAncestors: ["'none'"], + objectSrc: ["'none'"], + reportUri: '/__cspreport__' + } + }; + if (config.fxa_client_id) { + csp.directives.connectSrc.push('https://accounts.firefox.com'); + csp.directives.connectSrc.push('https://*.accounts.firefox.com'); + csp.directives.imgSrc.push('https://firefoxusercontent.com'); + csp.directives.imgSrc.push('https://secure.gravatar.com'); + } + if (config.sentry_id) { + csp.directives.connectSrc.push(config.sentry_host); + } + if ( + /^https:\/\/.*\.dev\.lcip\.org$/.test(config.base_url) || + /^https:\/\/.*\.send\.nonprod\.cloudops\.mozgcp\.net$/.test( + config.base_url + ) + ) { + csp.directives.connectSrc.push('https://*.dev.lcip.org'); + csp.directives.imgSrc.push('https://*.dev.lcip.org'); + } + if (config.fxa_csp_oauth_url != '') { + csp.directives.connectSrc.push(config.fxa_csp_oauth_url); + } + if (config.fxa_csp_content_url != '') { + csp.directives.connectSrc.push(config.fxa_csp_content_url); + } + if (config.fxa_csp_profile_url != '') { + csp.directives.connectSrc.push(config.fxa_csp_profile_url); + } + if (config.fxa_csp_profileimage_url != '') { + csp.directives.imgSrc.push(config.fxa_csp_profileimage_url); + } + + app.use(helmet.contentSecurityPolicy(csp)); } + app.use(function(req, res, next) { res.set('Pragma', 'no-cache'); res.set( @@ -73,6 +93,19 @@ module.exports = function(app) { ); next(); }); + app.use(function(req, res, next) { + try { + // set by the load balancer + const [country, state] = req.header('X-Client-Geo-Location').split(','); + req.geo = { + country, + state + }; + } catch (e) { + req.geo = {}; + } + next(); + }); app.use(bodyParser.json()); app.use(bodyParser.text()); app.get('/', language, pages.index); @@ -83,29 +116,32 @@ module.exports = function(app) { app.get('/oauth', language, pages.blank); app.get('/legal', language, pages.legal); app.get('/login', language, pages.index); + app.get('/report', language, pages.blank); app.get('/app.webmanifest', language, require('./webmanifest')); app.get(`/download/:id${ID_REGEX}`, language, pages.download); app.get('/unsupported/:reason', language, pages.unsupported); - app.get(`/api/download/:id${ID_REGEX}`, auth.hmac, require('./download')); + app.get(`/api/download/token/:id${ID_REGEX}`, auth.hmac, require('./token')); + app.get(`/api/download/:id${ID_REGEX}`, auth.dlToken, require('./download')); app.get( `/api/download/blob/:id${ID_REGEX}`, - auth.hmac, + auth.dlToken, require('./download') ); + app.post( + `/api/download/done/:id${ID_REGEX}`, + auth.dlToken, + require('./done.js') + ); app.get(`/api/exists/:id${ID_REGEX}`, require('./exists')); app.get(`/api/metadata/:id${ID_REGEX}`, auth.hmac, require('./metadata')); app.get('/api/filelist/:id([\\w-]{16})', auth.fxa, filelist.get); app.post('/api/filelist/:id([\\w-]{16})', auth.fxa, filelist.post); - app.post('/api/upload', auth.fxa, require('./upload')); + // app.post('/api/upload', auth.fxa, require('./upload')); app.post(`/api/delete/:id${ID_REGEX}`, auth.owner, require('./delete')); app.post(`/api/password/:id${ID_REGEX}`, auth.owner, require('./password')); - app.post( - `/api/params/:id${ID_REGEX}`, - auth.owner, - auth.fxa, - require('./params') - ); + app.post(`/api/params/:id${ID_REGEX}`, auth.owner, require('./params')); app.post(`/api/info/:id${ID_REGEX}`, auth.owner, require('./info')); + app.post(`/api/report/:id${ID_REGEX}`, auth.hmac, require('./report')); app.post('/api/metrics', require('./metrics')); app.get('/__version__', function(req, res) { // eslint-disable-next-line node/no-missing-require diff --git a/server/routes/metadata.js b/server/routes/metadata.js index 2e50537cf..ff4d130ad 100644 --- a/server/routes/metadata.js +++ b/server/routes/metadata.js @@ -4,10 +4,14 @@ module.exports = async function(req, res) { const id = req.params.id; const meta = req.meta; try { + if (meta.dead && !meta.flagged) { + return res.sendStatus(404); + } const ttl = await storage.ttl(id); res.send({ metadata: meta.metadata, - finalDownload: meta.dl + 1 === meta.dlimit, + flagged: !!meta.flagged, + finalDownload: meta.dlToken + 1 === meta.dlimit, ttl }); } catch (e) { diff --git a/server/routes/metrics.js b/server/routes/metrics.js index 059e330e8..0f6f64aae 100644 --- a/server/routes/metrics.js +++ b/server/routes/metrics.js @@ -12,7 +12,8 @@ module.exports = async function(req, res) { data.session_id + deltaT, deltaT, data.platform, - req.ip + req.geo.country, + req.geo.state ) ); const status = await sendBatch(events); diff --git a/server/routes/pages.js b/server/routes/pages.js index 9fe6e530a..5b1ba58df 100644 --- a/server/routes/pages.js +++ b/server/routes/pages.js @@ -23,14 +23,17 @@ module.exports = { const id = req.params.id; const appState = await state(req); try { - const { nonce, pwd } = await storage.metadata(id); + const { nonce, pwd, dead, flagged } = await storage.metadata(id); + if (dead && !flagged) { + return next(); + } res.set('WWW-Authenticate', `send-v1 ${nonce}`); res.send( stripEvents( routes().toString( `/download/${id}`, Object.assign(appState, { - downloadMetadata: { nonce, pwd } + downloadMetadata: { nonce, pwd, flagged } }) ) ) @@ -56,6 +59,15 @@ module.exports = { notfound: async function(req, res) { const appState = await state(req); - res.status(404).send(stripEvents(routes().toString('/404', appState))); + res + .status(404) + .send( + stripEvents( + routes().toString( + '/404', + Object.assign(appState, { downloadMetadata: { status: 404 } }) + ) + ) + ); } }; diff --git a/server/routes/params.js b/server/routes/params.js index 08e22f254..ac9118867 100644 --- a/server/routes/params.js +++ b/server/routes/params.js @@ -2,7 +2,7 @@ const config = require('../config'); const storage = require('../storage'); module.exports = function(req, res) { - const max = req.user ? config.max_downloads : config.anon_max_downloads; + const max = req.meta.fxa ? config.max_downloads : config.anon_max_downloads; const dlimit = req.body.dlimit; if (!dlimit || dlimit > max) { return res.sendStatus(400); diff --git a/server/routes/password.js b/server/routes/password.js index bf2f5679b..7662b6dd8 100644 --- a/server/routes/password.js +++ b/server/routes/password.js @@ -9,7 +9,7 @@ module.exports = function(req, res) { try { storage.setField(id, 'auth', auth); - storage.setField(id, 'pwd', true); + storage.setField(id, 'pwd', 1); res.sendStatus(200); } catch (e) { return res.sendStatus(404); diff --git a/server/routes/report.js b/server/routes/report.js new file mode 100644 index 000000000..5598e926b --- /dev/null +++ b/server/routes/report.js @@ -0,0 +1,24 @@ +const storage = require('../storage'); +const { statReportEvent } = require('../amplitude'); + +module.exports = async function(req, res) { + try { + const id = req.params.id; + const meta = await storage.metadata(id); + storage.flag(id); + statReportEvent({ + id, + ip: req.ip, + country: req.geo.country, + state: req.geo.state, + owner: meta.owner, + reason: req.body.reason, + download_limit: meta.dlimit, + download_count: meta.dl, + agent: req.ua.browser.name || req.ua.ua.substring(0, 6) + }); + res.sendStatus(200); + } catch (e) { + res.sendStatus(404); + } +}; diff --git a/server/routes/token.js b/server/routes/token.js new file mode 100644 index 000000000..3ec5cf9b7 --- /dev/null +++ b/server/routes/token.js @@ -0,0 +1,17 @@ +module.exports = async function(req, res) { + const meta = req.meta; + try { + if (meta.dead || meta.flagged) { + return res.sendStatus(404); + } + const token = await meta.getDownloadToken(); + res.send({ + token + }); + } catch (e) { + if (e.message === 'limit') { + return res.sendStatus(403); + } + res.sendStatus(404); + } +}; diff --git a/server/routes/ws.js b/server/routes/ws.js index 32ea79050..dfd9d4d29 100644 --- a/server/routes/ws.js +++ b/server/routes/ws.js @@ -41,6 +41,14 @@ module.exports = function(ws, req) { ? config.max_downloads : config.anon_max_downloads; + if (config.fxa_required && !user) { + ws.send( + JSON.stringify({ + error: 401 + }) + ); + return ws.close(); + } if ( !metadata || !auth || @@ -58,6 +66,7 @@ module.exports = function(ws, req) { const meta = { owner, + fxa: user ? 1 : 0, metadata, dlimit, auth: auth.split(' ')[1], @@ -103,6 +112,8 @@ module.exports = function(ws, req) { statUploadEvent({ id: newId, ip: req.ip, + country: req.geo.country, + state: req.geo.state, owner, dlimit, timeLimit, diff --git a/server/state.js b/server/state.js index 6947a7215..914ffe31f 100644 --- a/server/state.js +++ b/server/state.js @@ -15,7 +15,11 @@ module.exports = async function(req) { try { authConfig = await getFxaConfig(); authConfig.client_id = config.fxa_client_id; + authConfig.fxa_required = config.fxa_required; } catch (e) { + if (config.auth_required) { + throw new Error('fxa_required is set but no config was found'); + } // continue without accounts } } diff --git a/server/storage/fs.js b/server/storage/fs.js index aa6da7446..8f0366a30 100644 --- a/server/storage/fs.js +++ b/server/storage/fs.js @@ -1,10 +1,8 @@ -const fs = require('fs'); +const fss = require('fs'); +const fs = fss.promises; const path = require('path'); -const promisify = require('util').promisify; const mkdirp = require('mkdirp'); -const stat = promisify(fs.stat); - class FSStorage { constructor(config, log) { this.log = log; @@ -13,32 +11,36 @@ class FSStorage { } async length(id) { - const result = await stat(path.join(this.dir, id)); + const result = await fs.stat(path.join(this.dir, id)); return result.size; } getStream(id) { - return fs.createReadStream(path.join(this.dir, id)); + return fss.createReadStream(path.join(this.dir, id)); } set(id, file) { return new Promise((resolve, reject) => { const filepath = path.join(this.dir, id); - const fstream = fs.createWriteStream(filepath); + const fstream = fss.createWriteStream(filepath); file.pipe(fstream); file.on('error', err => { fstream.destroy(err); }); fstream.on('error', err => { - fs.unlinkSync(filepath); + this.del(id); reject(err); }); fstream.on('finish', resolve); }); } - del(id) { - return Promise.resolve(fs.unlinkSync(path.join(this.dir, id))); + async del(id) { + try { + await fs.unlink(path.join(this.dir, id)); + } catch (e) { + // ignore local fs issues + } } ping() { diff --git a/server/storage/index.js b/server/storage/index.js index 3e46c5c15..2a0831e67 100644 --- a/server/storage/index.js +++ b/server/storage/index.js @@ -32,28 +32,42 @@ class DB { return Math.ceil(result) * 1000; } - async getPrefixedId(id) { - const prefix = await this.redis.hgetAsync(id, 'prefix'); - return `${prefix}-${id}`; + async getPrefixedInfo(id) { + const [prefix, dead, flagged] = await this.redis.hmgetAsync( + id, + 'prefix', + 'dead', + 'flagged' + ); + return { + filePath: `${prefix}-${id}`, + flagged, + dead + }; } async length(id) { - const filePath = await this.getPrefixedId(id); + const { filePath } = await this.getPrefixedInfo(id); return this.storage.length(filePath); } async get(id) { - const filePath = await this.getPrefixedId(id); - return this.storage.getStream(filePath); + const info = await this.getPrefixedInfo(id); + if (info.dead || info.flagged) { + throw new Error(info.flagged ? 'flagged' : 'dead'); + } + const length = await this.storage.length(info.filePath); + return { length, stream: this.storage.getStream(info.filePath) }; } async set(id, file, meta, expireSeconds = config.default_expire_seconds) { const prefix = getPrefix(expireSeconds); const filePath = `${prefix}-${id}`; await this.storage.set(filePath, file); - this.redis.hset(id, 'prefix', prefix); if (meta) { - this.redis.hmset(id, meta); + this.redis.hmset(id, { prefix, ...meta }); + } else { + this.redis.hset(id, 'prefix', prefix); } this.redis.expire(id, expireSeconds); } @@ -62,14 +76,27 @@ class DB { this.redis.hset(id, key, value); } - incrementField(id, key, increment = 1) { - this.redis.hincrby(id, key, increment); + async incrementField(id, key, increment = 1) { + return await this.redis.hincrbyAsync(id, key, increment); + } + + async kill(id) { + const { filePath, dead } = await this.getPrefixedInfo(id); + if (!dead) { + this.redis.hset(id, 'dead', 1); + this.storage.del(filePath); + } + } + + async flag(id) { + await this.kill(id); + this.redis.hset(id, 'flagged', 1); } async del(id) { - const filePath = await this.getPrefixedId(id); - this.storage.del(filePath); + const { filePath } = await this.getPrefixedInfo(id); this.redis.del(id); + this.storage.del(filePath); } async ping() { @@ -79,7 +106,7 @@ class DB { async metadata(id) { const result = await this.redis.hgetallAsync(id); - return result && new Metadata(result); + return result && new Metadata({ id, ...result }, this); } } diff --git a/server/storage/redis.js b/server/storage/redis.js index 645a8e7cb..4ab57c58e 100644 --- a/server/storage/redis.js +++ b/server/storage/redis.js @@ -2,7 +2,7 @@ const promisify = require('util').promisify; module.exports = function(config) { const redis_lib = - config.env === 'development' && config.redis_host === 'localhost' + config.env === 'development' && config.redis_host === 'mock' ? 'redis-mock' : 'redis'; @@ -11,18 +11,21 @@ module.exports = function(config) { const client = redis.createClient({ host: config.redis_host, retry_strategy: options => { - if (options.total_retry_time > 10000) { + if (options.total_retry_time > config.redis_retry_time) { client.emit('error', 'Retry time exhausted'); return new Error('Retry time exhausted'); } - return 500; + return config.redis_retry_delay; } }); client.ttlAsync = promisify(client.ttl); client.hgetallAsync = promisify(client.hgetall); client.hgetAsync = promisify(client.hget); + client.hincrbyAsync = promisify(client.hincrby); + client.hmgetAsync = promisify(client.hmget); client.pingAsync = promisify(client.ping); + client.existsAsync = promisify(client.exists); return client; }; diff --git a/server/storage/s3.js b/server/storage/s3.js index bb2b0100c..b181e5486 100644 --- a/server/storage/s3.js +++ b/server/storage/s3.js @@ -1,25 +1,31 @@ const AWS = require('aws-sdk'); -const s3 = new AWS.S3(); class S3Storage { constructor(config, log) { this.bucket = config.s3_bucket; this.log = log; + const cfg = {}; + if (config.s3_endpoint != '') { + cfg['endpoint'] = config.s3_endpoint; + } + cfg['s3ForcePathStyle'] = config.s3_use_path_style_endpoint + AWS.config.update(cfg); + this.s3 = new AWS.S3(); } async length(id) { - const result = await s3 + const result = await this.s3 .headObject({ Bucket: this.bucket, Key: id }) .promise(); - return result.ContentLength; + return Number(result.ContentLength); } getStream(id) { - return s3.getObject({ Bucket: this.bucket, Key: id }).createReadStream(); + return this.s3.getObject({ Bucket: this.bucket, Key: id }).createReadStream(); } set(id, file) { - const upload = s3.upload({ + const upload = this.s3.upload({ Bucket: this.bucket, Key: id, Body: file @@ -29,11 +35,11 @@ class S3Storage { } del(id) { - return s3.deleteObject({ Bucket: this.bucket, Key: id }).promise(); + return this.s3.deleteObject({ Bucket: this.bucket, Key: id }).promise(); } ping() { - return s3.headBucket({ Bucket: this.bucket }).promise(); + return this.s3.headBucket({ Bucket: this.bucket }).promise(); } } diff --git a/tailwind.config.js b/tailwind.config.js index 470695f55..b657c728b 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -259,6 +259,14 @@ module.exports = { full: '100%', screen: '100vh' }, + flex: { + '1': '1 1 0%', + auto: '1 1 auto', + initial: '0 1 auto', + none: 'none', + half: '0 0 50%', + full: '0 0 100%' + }, minWidth: { '0': '0', full: '100%' diff --git a/test/backend/delete-tests.js b/test/backend/delete-tests.js index 2984b34bf..cd353d957 100644 --- a/test/backend/delete-tests.js +++ b/test/backend/delete-tests.js @@ -2,7 +2,7 @@ const sinon = require('sinon'); const proxyquire = require('proxyquire').noCallThru(); const storage = { - del: sinon.stub(), + kill: sinon.stub(), ttl: sinon.stub() }; @@ -24,19 +24,19 @@ const delRoute = proxyquire('../../server/routes/delete', { describe('/api/delete', function() { afterEach(function() { - storage.del.reset(); + storage.kill.reset(); }); - it('calls storage.del with the id parameter', async function() { + it('calls storage.kill with the id parameter', async function() { const req = request('x'); const res = response(); await delRoute(req, res); - sinon.assert.calledWith(storage.del, 'x'); + sinon.assert.calledWith(storage.kill, 'x'); sinon.assert.calledWith(res.sendStatus, 200); }); it('sends a 404 on failure', async function() { - storage.del.returns(Promise.reject(new Error())); + storage.kill.returns(Promise.reject(new Error())); const res = response(); await delRoute(request('x'), res); sinon.assert.calledWith(res.sendStatus, 404); diff --git a/test/backend/metadata-tests.js b/test/backend/metadata-tests.js index 9208b912b..5fbf6f533 100644 --- a/test/backend/metadata-tests.js +++ b/test/backend/metadata-tests.js @@ -6,7 +6,7 @@ const storage = { length: sinon.stub() }; -function request(id, meta) { +function request(id, meta = {}) { return { params: { id }, meta @@ -48,7 +48,7 @@ describe('/api/metadata', function() { storage.ttl.returns(Promise.resolve(123)); const meta = { dlimit: 1, - dl: 0, + dlToken: 0, metadata: 'foo' }; const res = response(); diff --git a/test/backend/params-tests.js b/test/backend/params-tests.js index 1ff38b2fc..61dd14e05 100644 --- a/test/backend/params-tests.js +++ b/test/backend/params-tests.js @@ -8,6 +8,7 @@ const storage = { function request(id) { return { params: { id }, + meta: { fxa: false }, body: {} }; } diff --git a/test/backend/password-tests.js b/test/backend/password-tests.js index 03c76c4c8..e52503b94 100644 --- a/test/backend/password-tests.js +++ b/test/backend/password-tests.js @@ -32,7 +32,7 @@ describe('/api/password', function() { const res = response(); passwordRoute(req, res); sinon.assert.calledWith(storage.setField, 'x', 'auth', 'z'); - sinon.assert.calledWith(storage.setField, 'x', 'pwd', true); + sinon.assert.calledWith(storage.setField, 'x', 'pwd', 1); sinon.assert.calledWith(res.sendStatus, 200); }); diff --git a/test/backend/s3-tests.js b/test/backend/s3-tests.js index 997b7c34a..9e6642fd0 100644 --- a/test/backend/s3-tests.js +++ b/test/backend/s3-tests.js @@ -22,6 +22,9 @@ const s3Stub = { }; const awsStub = { + config: { + update: sinon.stub() + }, S3: function() { return s3Stub; } diff --git a/test/backend/storage-tests.js b/test/backend/storage-tests.js index 9f8408cfc..eee22cb4c 100644 --- a/test/backend/storage-tests.js +++ b/test/backend/storage-tests.js @@ -25,7 +25,7 @@ const config = { default_expire_seconds: 20, expire_times_seconds: [10, 20, 30], env: 'development', - redis_host: 'localhost' + redis_host: 'mock' }; const storage = proxyquire('../../server/storage', { @@ -54,7 +54,7 @@ describe('Storage', function() { describe('get', function() { it('returns a stream', async function() { - const s = await storage.get('x'); + const { stream: s } = await storage.get('x'); assert.equal(s, stream); }); }); @@ -70,17 +70,17 @@ describe('Storage', function() { it('adds right prefix based on expire time', async function() { await storage.set('x', null, { foo: 'bar' }, 300); - const path_x = await storage.getPrefixedId('x'); + const { filePath: path_x } = await storage.getPrefixedInfo('x'); assert.equal(path_x, '1-x'); await storage.del('x'); await storage.set('y', null, { foo: 'bar' }, 86400); - const path_y = await storage.getPrefixedId('y'); + const { filePath: path_y } = await storage.getPrefixedInfo('y'); assert.equal(path_y, '1-y'); await storage.del('y'); await storage.set('z', null, { foo: 'bar' }, 86400 * 7); - const path_z = await storage.getPrefixedId('z'); + const { filePath: path_z } = await storage.getPrefixedInfo('z'); assert.equal(path_z, '7-z'); await storage.del('z'); }); @@ -123,9 +123,11 @@ describe('Storage', function() { describe('metadata', function() { it('returns all metadata fields', async function() { const m = { - pwd: true, + id: 'a1', + pwd: 0, dl: 1, dlimit: 1, + fxa: 1, auth: 'foo', metadata: 'bar', nonce: 'baz', @@ -133,7 +135,18 @@ describe('Storage', function() { }; await storage.set('x', null, m); const meta = await storage.metadata('x'); - assert.deepEqual(meta, m); + assert.deepEqual( + { ...meta, storage: 'excluded' }, + { + ...m, + dead: false, + flagged: false, + dlToken: 0, + fxa: true, + pwd: false, + storage: 'excluded' + } + ); }); }); }); diff --git a/test/frontend/tests/streaming-tests.js b/test/frontend/tests/streaming-tests.js index 0e6905056..b2a88f451 100644 --- a/test/frontend/tests/streaming-tests.js +++ b/test/frontend/tests/streaming-tests.js @@ -1,12 +1,11 @@ const ece = require('http_ece'); -require('buffer'); import assert from 'assert'; import Archive from '../../../app/archive'; import { b64ToArray } from '../../../app/utils'; import { blobStream, concatStream } from '../../../app/streams'; import { decryptStream, encryptStream } from '../../../app/ece.js'; -import { encryptedSize } from '../../../app/utils'; +import { encryptedSize, concat } from '../../../app/utils'; const rs = 36; @@ -75,15 +74,15 @@ describe('Streaming', function() { const encStream = encryptStream(stream, key, rs, salt); const reader = encStream.getReader(); - let result = Buffer.from([]); + let result = new Uint8Array(0); let state = await reader.read(); while (!state.done) { - result = Buffer.concat([result, state.value]); + result = concat(result, state.value); state = await reader.read(); } - assert.deepEqual(result, encrypted); + assert.deepEqual(result, new Uint8Array(encrypted)); }); it('can decrypt', async function() { @@ -91,15 +90,14 @@ describe('Streaming', function() { const decStream = decryptStream(stream, key, rs); const reader = decStream.getReader(); - let result = Buffer.from([]); + let result = new Uint8Array(0); let state = await reader.read(); while (!state.done) { - result = Buffer.concat([result, state.value]); + result = concat(result, state.value); state = await reader.read(); } - - assert.deepEqual(result, decrypted); + assert.deepEqual(result, new Uint8Array(decrypted)); }); }); diff --git a/test/frontend/tests/workflow-tests.js b/test/frontend/tests/workflow-tests.js index d96e4ba02..5885c126d 100644 --- a/test/frontend/tests/workflow-tests.js +++ b/test/frontend/tests/workflow-tests.js @@ -2,12 +2,13 @@ import assert from 'assert'; import Archive from '../../../app/archive'; import FileSender from '../../../app/fileSender'; import FileReceiver from '../../../app/fileReceiver'; +import storage from '../../../app/storage'; const headless = /Headless/.test(navigator.userAgent); // TODO: save on headless doesn't work as it used to since it now // follows a link instead of fetch. Maybe there's a way to make it // work? For now always set noSave. -const options = { noSave: true || !headless, stream: true }; // only run the saveFile code if headless +const options = { noSave: true || !headless, stream: true, storage }; // only run the saveFile code if headless // FileSender uses a File in real life but a Blob works for testing const blob = new Blob([new ArrayBuffer(1024 * 128)], { type: 'text/plain' }); @@ -181,14 +182,15 @@ describe('Upload / Download flow', function() { it('can allow multiple downloads', async function() { const fs = new FileSender(); - const file = await fs.upload(archive); + const a = new Archive([blob]); + a.dlimit = 2; + const file = await fs.upload(a); const fr = new FileReceiver({ secretKey: file.toJSON().secretKey, id: file.id, nonce: file.keychain.nonce, requiresPassword: false }); - await file.changeLimit(2); await fr.getMetadata(); await fr.download(options); await file.updateDownloadCount(); diff --git a/webpack.config.js b/webpack.config.js index 0f49c234c..0c7d6c1a3 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -12,6 +12,7 @@ const webJsOptions = { [ '@babel/preset-env', { + bugfixes: true, useBuiltIns: 'entry', corejs: 3 } @@ -78,9 +79,9 @@ const serviceWorker = { const web = { target: 'web', entry: { - app: ['./app/main.js'], - android: ['./android/android.js'], - ios: ['./ios/ios.js'] + app: ['./app/main.js'] + // android: ['./android/android.js'], + // ios: ['./ios/ios.js'] }, output: { chunkFilename: '[name].[contenthash:8].js', @@ -207,7 +208,7 @@ const web = { host: '0.0.0.0', proxy: { '/api/ws': { - target: 'ws://localhost:8081', + target: 'ws://localhost:1338', ws: true, secure: false }