Merge pull request #1496 from dannycoates/dear-nice-things-too

Begin implementing a reporting mechanism
This commit is contained in:
Danny Coates 2020-07-24 12:35:28 -07:00 committed by GitHub
commit 0a8663aa51
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
41 changed files with 782 additions and 186 deletions

View File

@ -127,10 +127,10 @@ export async function metadata(id, keychain) {
return { return {
size: meta.size, size: meta.size,
ttl: data.ttl, ttl: data.ttl,
iv: meta.iv,
name: meta.name, name: meta.name,
type: meta.type, type: meta.type,
manifest: meta.manifest manifest: meta.manifest,
flagged: data.flagged
}; };
} }
throw new Error(result.response.status); throw new Error(result.response.status);
@ -438,3 +438,16 @@ export async function getConstants() {
throw new Error(response.status); throw new Error(response.status);
} }
export async function reportLink(id, key, reason) {
const response = await fetch(
getApiUrl(`/api/report/${id}`),
post({ key, reason })
);
if (response.ok) {
return;
}
throw new Error(response.status);
}

View File

@ -1,5 +1,6 @@
import FileSender from './fileSender'; import FileSender from './fileSender';
import FileReceiver from './fileReceiver'; import FileReceiver from './fileReceiver';
import { reportLink } from './api';
import { copyToClipboard, delay, openLinksInNewTab, percent } from './utils'; import { copyToClipboard, delay, openLinksInNewTab, percent } from './utils';
import * as metrics from './metrics'; import * as metrics from './metrics';
import { bytes, locale } from './utils'; import { bytes, locale } from './utils';
@ -306,6 +307,26 @@ export default function(state, emitter) {
render(); render();
}); });
emitter.on('report', async ({ reason }) => {
try {
const file = state.fileInfo;
if (!file) {
// TODO
emitter.emit('pushState', '/error');
return render();
}
await reportLink(file.id, file.secretKey, reason);
render();
} catch (err) {
console.error(err);
if (err.message === '404') {
state.fileInfo = { reported: true };
return render();
}
emitter.emit('pushState', '/error');
}
});
setInterval(() => { setInterval(() => {
// poll for updates of the upload list // poll for updates of the upload list
if (!state.modal && state.route === '/') { if (!state.modal && state.route === '/') {

View File

@ -47,9 +47,9 @@ export default class FileReceiver extends Nanobus {
const meta = await metadata(this.fileInfo.id, this.keychain); const meta = await metadata(this.fileInfo.id, this.keychain);
this.fileInfo.name = meta.name; this.fileInfo.name = meta.name;
this.fileInfo.type = meta.type; this.fileInfo.type = meta.type;
this.fileInfo.iv = meta.iv;
this.fileInfo.size = +meta.size; this.fileInfo.size = +meta.size;
this.fileInfo.manifest = meta.manifest; this.fileInfo.manifest = meta.manifest;
this.fileInfo.flagged = meta.flagged;
this.state = 'ready'; this.state = 'ready';
} }

View File

@ -55,6 +55,12 @@ body {
@apply bg-blue-70; @apply bg-blue-70;
} }
.btn:disabled {
@apply bg-grey-transparent;
cursor: not-allowed;
}
.checkbox { .checkbox {
@apply leading-normal; @apply leading-normal;
@apply select-none; @apply select-none;
@ -138,21 +144,6 @@ footer li:hover {
text-decoration: underline; 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 { .link-blue {
@apply text-blue-60; @apply text-blue-60;
} }
@ -175,6 +166,10 @@ footer li:hover {
height: unset; height: unset;
} }
.dl-bg {
filter: grayscale(1) opacity(0.15);
}
.main { .main {
display: flex; display: flex;
position: relative; position: relative;
@ -322,6 +317,10 @@ select {
@apply bg-blue-50; @apply bg-blue-50;
} }
.btn:disabled {
@apply bg-grey-80;
}
.link-blue { .link-blue {
@apply text-blue-40; @apply text-blue-40;
} }
@ -392,48 +391,3 @@ select {
.signin:hover:active { .signin:hover:active {
transform: scale(0.9375); 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 */

View File

@ -14,6 +14,7 @@ module.exports = function(app = choo({ hash: true })) {
emit('authenticate', state.query.code, state.query.state); emit('authenticate', state.query.code, state.query.state);
}); });
app.route('/login', body(require('./ui/home'))); app.route('/login', body(require('./ui/home')));
app.route('/report', body(require('./ui/report')));
app.route('*', body(require('./ui/notFound'))); app.route('*', body(require('./ui/notFound')));
return app; return app;
}; };

View File

@ -482,6 +482,11 @@ module.exports.empty = function(state, emit) {
> >
${state.translate('addFilesButton')} ${state.translate('addFilesButton')}
</label> </label>
<p
class="font-normal text-sm text-grey-50 dark:text-grey-40 my-6 mx-12 text-center max-w-sm leading-loose"
>
${state.translate('trustWarningMessage')}
</p>
${upsell} ${upsell}
</send-upload-area> </send-upload-area>
`; `;
@ -517,13 +522,27 @@ module.exports.preview = function(state, emit) {
`; `;
return html` return html`
<send-archive <send-archive
class="flex flex-col max-h-full bg-white p-4 w-full md:w-128 dark:bg-grey-90" class="flex flex-col max-h-full bg-white w-full dark:bg-grey-90"
> >
<div class="border rounded py-3 px-6 dark:border-grey-70"> <div class="border rounded py-3 px-4 dark:border-grey-70">
${archiveInfo(archive)} ${details} ${archiveInfo(archive)} ${details}
</div> </div>
<div class="checkbox inline-block mt-6 mx-auto">
<input
id="trust-download"
type="checkbox"
autocomplete="off"
onchange="${toggleDownloadEnabled}"
/>
<label for="trust-download">
${state.translate('downloadTrustCheckbox', {
count: archive.manifest.files.length
})}
</label>
</div>
<button <button
id="download-btn" id="download-btn"
disabled
class="btn rounded-lg mt-4 w-full flex-shrink-0 focus:outline" class="btn rounded-lg mt-4 w-full flex-shrink-0 focus:outline"
title="${state.translate('downloadButtonLabel')}" title="${state.translate('downloadButtonLabel')}"
onclick=${download} onclick=${download}
@ -533,6 +552,13 @@ module.exports.preview = function(state, emit) {
</send-archive> </send-archive>
`; `;
function toggleDownloadEnabled(event) {
event.stopPropagation();
const checked = event.target.checked;
const btn = document.getElementById('download-btn');
btn.disabled = !checked;
}
function download(event) { function download(event) {
event.preventDefault(); event.preventDefault();
event.target.disabled = true; event.target.disabled = true;

View File

@ -10,11 +10,9 @@ module.exports = function(name, url) {
<h1 class="text-3xl font-bold my-4"> <h1 class="text-3xl font-bold my-4">
${state.translate('notifyUploadEncryptDone')} ${state.translate('notifyUploadEncryptDone')}
</h1> </h1>
<p <p class="font-normal leading-normal text-grey-80 dark:text-grey-40">
class="font-normal leading-normal text-grey-80 word-break-all dark:text-grey-40"
>
${state.translate('copyLinkDescription')} <br /> ${state.translate('copyLinkDescription')} <br />
${name} <span class="word-break-all">${name}</span>
</p> </p>
<input <input
type="text" type="text"

View File

@ -1,5 +1,6 @@
/* global downloadMetadata */ /* global downloadMetadata */
const html = require('choo/html'); const html = require('choo/html');
const assets = require('../../common/assets');
const archiveTile = require('./archiveTile'); const archiveTile = require('./archiveTile');
const modal = require('./modal'); const modal = require('./modal');
const noStreams = require('./noStreams'); const noStreams = require('./noStreams');
@ -31,22 +32,53 @@ function downloading(state, emit) {
} }
function preview(state, emit) { function preview(state, emit) {
if (state.fileInfo.flagged) {
return html`
<div
class="flex flex-col w-full max-w-md h-full mx-auto items-center justify-center"
>
<h1 class="text-xl font-bold">${state.translate('downloadFlagged')}</h1>
</div>
`;
}
if (!state.capabilities.streamDownload && state.fileInfo.size > BIG_SIZE) { if (!state.capabilities.streamDownload && state.fileInfo.size > BIG_SIZE) {
return noStreams(state, emit); return noStreams(state, emit);
} }
return html` return html`
<div <div
class="flex flex-col w-full max-w-md h-full mx-auto items-center justify-center" class="w-full overflow-hidden md:flex md:flex-row items-stretch md:flex-1"
> >
<h1 class="text-3xl font-bold mb-4"> <div
${state.translate('downloadTitle')} class="px-2 w-full md:px-0 flex-half md:flex md:flex-col mt-12 md:pr-8 pb-4"
</h1>
<p
class="w-full text-grey-80 text-center leading-normal dark:text-grey-40"
> >
${state.translate('downloadDescription')} <h1 class="text-3xl font-bold mb-4 text-center md:text-left">
</p> ${state.translate('downloadTitle')}
${archiveTile.preview(state, emit)} </h1>
<p
class="text-grey-80 leading-normal dark:text-grey-40 mb-4 text-center md:text-left"
>
${state.translate('downloadDescription')}
</p>
<p
class="text-grey-80 leading-normal dark:text-grey-40 font-semibold text-center md:mb-8 md:text-left"
>
${state.translate('downloadConfirmDescription')}
</p>
<img
class="hidden md:block dl-bg w-full"
src="${assets.get('intro.svg')}"
/>
</div>
<div
class="w-full flex-half flex-half md:flex md:flex-col md:justify-center"
>
${archiveTile.preview(state, emit)}
<a href="/report" class="link-blue mt-4 text-center block"
>${state.translate('reportFile', {
count: state.fileInfo.manifest.files.length
})}</a
>
</div>
</div> </div>
`; `;
} }
@ -83,7 +115,7 @@ module.exports = function(state, emit) {
<main class="main"> <main class="main">
${state.modal && modal(state, emit)} ${state.modal && modal(state, emit)}
<section <section
class="relative h-full w-full p-6 md:p-8 md:rounded-xl md:shadow-big" class="relative h-full w-full p-6 md:p-8 md:rounded-xl md:shadow-big md:flex md:flex-col"
> >
${content} ${content}
</section> </section>

View File

@ -10,7 +10,7 @@ module.exports = function(state) {
<h1 class="text-center text-3xl font-bold my-2"> <h1 class="text-center text-3xl font-bold my-2">
${state.translate('downloadFinish')} ${state.translate('downloadFinish')}
</h1> </h1>
<img src="${assets.get('completed.svg')}" class="my-12 h-48" /> <img src="${assets.get('completed.svg')}" class="my-8 h-48" />
<p class="text-grey-80 leading-normal dark:text-grey-40"> <p class="text-grey-80 leading-normal dark:text-grey-40">
${state.translate('trySendDescription')} ${state.translate('trySendDescription')}
</p> </p>
@ -19,6 +19,9 @@ module.exports = function(state) {
>${state.translate('sendYourFilesLink')}</a >${state.translate('sendYourFilesLink')}</a
> >
</p> </p>
<p class="">
<a href="/report" class="link-blue">${state.translate('reportFile')}</a>
</p>
</div> </div>
`; `;
}; };

58
app/ui/downloadDialog.js Normal file
View File

@ -0,0 +1,58 @@
const html = require('choo/html');
module.exports = function() {
return function(state, emit, close) {
const archive = state.fileInfo;
return html`
<send-download-dialog
class="flex flex-col w-full max-w-sm h-full mx-auto items-center justify-center"
>
<h1 class="text-3xl font-bold mb-4">
${state.translate('downloadConfirmTitle')}
</h1>
<p
class="w-full text-grey-80 text-center leading-normal dark:text-grey-40 mb-8"
>
${state.translate('downloadConfirmDescription')}
</p>
<div class="checkbox inline-block mr-3 mb-8">
<input
id="trust-download"
type="checkbox"
autocomplete="off"
onchange="${toggleDownloadEnabled}"
/>
<label for="trust-download">
${state.translate('downloadTrustCheckbox')}
</label>
</div>
<button
id="download-btn"
disabled
class="btn rounded-lg w-full flex-shrink-0"
onclick="${download}"
title="${state.translate('downloadButtonLabel')}"
>
${state.translate('downloadButtonLabel')}
</button>
<a href="/report" class="link-blue mt-8"
>${state.translate('reportFile')}</a
>
</send-download-dialog>
`;
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);
}
};
};

View File

@ -1,7 +1,5 @@
const html = require('choo/html'); const html = require('choo/html');
const Component = require('choo/component'); const Component = require('choo/component');
const version = require('../../package.json').version;
const { browserName } = require('../utils');
class Footer extends Component { class Footer extends Component {
constructor(name, state) { constructor(name, state) {
@ -15,8 +13,6 @@ class Footer extends Component {
createElement() { createElement() {
const translate = this.state.translate; 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` return html`
<footer <footer
class="flex flex-col md:flex-row items-start w-full flex-none self-start p-6 md:p-8 font-medium text-xs text-grey-60 dark:text-grey-40 md:items-center justify-between" class="flex flex-col md:flex-row items-start w-full flex-none self-start p-6 md:p-8 font-medium text-xs text-grey-60 dark:text-grey-40 md:items-center justify-between"
@ -43,17 +39,6 @@ class Footer extends Component {
<li class="m-2"> <li class="m-2">
<a href="https://github.com/mozilla/send">GitHub </a> <a href="https://github.com/mozilla/send">GitHub </a>
</li> </li>
<li class="m-2">
<a
href="${feedbackUrl}"
rel="noreferrer noopener"
class="feedback-link"
alt="Feedback"
target="_blank"
>
${translate('siteFeedback')}
</a>
</li>
</ul> </ul>
</footer> </footer>
`; `;

View File

@ -5,6 +5,9 @@ const modal = require('./modal');
const intro = require('./intro'); const intro = require('./intro');
module.exports = function(state, emit) { module.exports = function(state, emit) {
if (state.user.loginRequired && !state.user.loggedIn) {
emit('signup-cta', 'required');
}
const archives = state.storage.files const archives = state.storage.files
.filter(archive => !archive.expired) .filter(archive => !archive.expired)
.map(archive => archiveTile(state, emit, archive)); .map(archive => archiveTile(state, emit, archive));

View File

@ -21,6 +21,11 @@ module.exports = function(state, emit) {
>${state.translate('sendYourFilesLink')}</a >${state.translate('sendYourFilesLink')}</a
> >
</p> </p>
<p class="">
<a href="/report" class="link-blue"
>${state.translate('reportFile')}</a
>
</p>
</section> </section>
</main> </main>
`; `;

132
app/ui/report.js Normal file
View File

@ -0,0 +1,132 @@
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`
<main class="main">
<section
class="flex flex-col items-center justify-center h-full w-full p-6 md:p-8 overflow-hidden md:rounded-xl md:shadow-big"
>
<p class="mb-4 leading-normal">
${state.translate('reportUnknownDescription')}
</p>
</section>
</main>
`;
}
if (file.reported) {
return html`
<main class="main">
<section
class="flex flex-col items-center justify-center h-full w-full p-6 md:p-8 overflow-hidden md:rounded-xl md:shadow-big"
>
<h1 class="text-center text-3xl font-bold my-2">
${state.translate('reportedTitle')}
</h1>
<p class="max-w-md text-center text-grey-80 leading-normal">
${state.translate('reportedDescription')}
</p>
<img src="${assets.get('notFound.svg')}" class="my-12" />
<p class="my-5">
<a href="/" class="btn rounded-lg flex items-center" role="button"
>${state.translate('okButton')}</a
>
</p>
</section>
</main>
`;
}
return html`
<main class="main">
<section
class="relative h-full w-full p-6 md:p-8 md:rounded-xl md:shadow-big"
>
<div
class="flex flex-col w-full max-w-sm h-full mx-auto items-center justify-center"
>
<h1 class="text-2xl font-bold mb-4">
${state.translate('reportFile')}
</h1>
<p class="mb-4 leading-normal font-semibold">
${state.translate('reportDescription')}
</p>
<form onsubmit="${report}" data-no-csrf>
<fieldset onchange="${optionChanged}">
<ul
class="list-none p-4 mb-6 rounded-sm bg-grey-10 dark:bg-black"
>
${REPORTABLES.map(
reportable =>
html`
<li class="mb-2 leading-normal">
<label
for="${reportable.toLowerCase()}"
class="flex items-center"
>
<input
type="radio"
name="reason"
id="${reportable.toLowerCase()}"
value="${reportable.toLowerCase()}"
class="mr-2 my-2 w-4 h-4"
/>
${state.translate(`reportReason${reportable}`)}
</label>
</li>
`
)}
<li class="mt-4 mb-2 leading-normal">
${raw(
replaceLinks(state.translate('reportReasonCopyright'), [
'https://www.mozilla.org/about/legal/report-infringement/'
])
)}
</li>
</ul>
</fieldset>
<input
type="submit"
disabled
class="btn rounded-lg w-full flex-shrink-0 focus:outline"
title="${state.translate('reportButton')}"
value="${state.translate('reportButton')}"
/>
</form>
</div>
</section>
</main>
`;
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>([^<]+)<\/a>/g,
(m, v) => `<a class="text-blue" href="${urls[i++]}">${v}</a>`
);
return `<p>${s}</p>`;
}
};

View File

@ -9,11 +9,9 @@ module.exports = function(name, url) {
<h1 class="text-3xl font-bold my-4"> <h1 class="text-3xl font-bold my-4">
${state.translate('notifyUploadEncryptDone')} ${state.translate('notifyUploadEncryptDone')}
</h1> </h1>
<p <p class="font-normal leading-normal text-grey-80 dark:text-grey-40">
class="font-normal leading-normal text-grey-80 word-break-all dark:text-grey-40"
>
${state.translate('shareLinkDescription')}<br /> ${state.translate('shareLinkDescription')}<br />
${name} <span class="word-break-all">${name}</span>
</p> </p>
<input <input
type="text" type="text"

View File

@ -94,6 +94,10 @@ export default class User {
: this.limits.ANON.MAX_DOWNLOADS; : this.limits.ANON.MAX_DOWNLOADS;
} }
get loginRequired() {
return this.authConfig.fxa_required;
}
async metricId() { async metricId() {
return this.loggedIn ? hashId(this.info.uid) : undefined; return this.loggedIn ? hashId(this.info.uid) : undefined;
} }

188
package-lock.json generated
View File

@ -1144,6 +1144,56 @@
"fastq": "^1.6.0" "fastq": "^1.6.0"
} }
}, },
"@peculiar/asn1-schema": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.0.5.tgz",
"integrity": "sha512-VIKJjsgMkv+yyWx3C+D4xo6/NeCg0XFBgNlavtkxELijV+aKAq53du5KkOJbeZtm1nn9CinQKny2PqL8zCfpeA==",
"requires": {
"@types/asn1js": "^0.0.1",
"asn1js": "^2.0.26",
"pvtsutils": "^1.0.10",
"tslib": "^1.11.1"
}
},
"@peculiar/json-schema": {
"version": "1.1.10",
"resolved": "https://registry.npmjs.org/@peculiar/json-schema/-/json-schema-1.1.10.tgz",
"integrity": "sha512-kbpnG9CkF1y6wwGkW7YtSA+yYK4X5uk4rAwsd1hxiaYE3Hkw2EsGlbGh/COkMLyFf+Fe830BoFiMSB3QnC/ItA==",
"requires": {
"tslib": "^1.11.1"
}
},
"@peculiar/webcrypto": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@peculiar/webcrypto/-/webcrypto-1.1.1.tgz",
"integrity": "sha512-Bu2XgOvzirnLcojZYs4KQ8hOLf2ETpa0NL6btQt5NgsAwctI6yVkzgYP+EcG7Mm579RBP+V0LM5rXyMlTVx23A==",
"requires": {
"@peculiar/asn1-schema": "^2.0.3",
"@peculiar/json-schema": "^1.1.10",
"pvtsutils": "^1.0.10",
"tslib": "^1.11.2",
"webcrypto-core": "^1.1.0"
},
"dependencies": {
"tslib": {
"version": "1.13.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz",
"integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q=="
},
"webcrypto-core": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/webcrypto-core/-/webcrypto-core-1.1.2.tgz",
"integrity": "sha512-LxM/dTcXr/ZnwwKLox0tGEOIqvP7KIJ4Hk/fFPX20tr1EgqTmpEFZinmu4FzoGVbs6e4jI1priQKCDrOBD3L6w==",
"requires": {
"@peculiar/asn1-schema": "^2.0.1",
"@peculiar/json-schema": "^1.1.10",
"asn1js": "^2.0.26",
"pvtsutils": "^1.0.10",
"tslib": "^1.11.2"
}
}
}
},
"@samverschueren/stream-to-observable": { "@samverschueren/stream-to-observable": {
"version": "0.3.0", "version": "0.3.0",
"resolved": "https://registry.npmjs.org/@samverschueren/stream-to-observable/-/stream-to-observable-0.3.0.tgz", "resolved": "https://registry.npmjs.org/@samverschueren/stream-to-observable/-/stream-to-observable-0.3.0.tgz",
@ -1296,6 +1346,14 @@
"resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz",
"integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==" "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw=="
}, },
"@types/asn1js": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/@types/asn1js/-/asn1js-0.0.1.tgz",
"integrity": "sha1-74uflwjLFjKhw6nNJ3F8qr55O8I=",
"requires": {
"@types/pvutils": "*"
}
},
"@types/color-name": { "@types/color-name": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz",
@ -1343,6 +1401,11 @@
"integrity": "sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA==", "integrity": "sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA==",
"dev": true "dev": true
}, },
"@types/pvutils": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/@types/pvutils/-/pvutils-0.0.2.tgz",
"integrity": "sha512-CgQAm7pjyeF3Gnv78ty4RBVIfluB+Td+2DR8iPaU0prF18pkzptHHP+DoKPfpsJYknKsVZyVsJEu5AuGgAqQ5w=="
},
"@types/q": { "@types/q": {
"version": "1.5.2", "version": "1.5.2",
"resolved": "https://registry.npmjs.org/@types/q/-/q-1.5.2.tgz", "resolved": "https://registry.npmjs.org/@types/q/-/q-1.5.2.tgz",
@ -1966,6 +2029,14 @@
"minimalistic-assert": "^1.0.0" "minimalistic-assert": "^1.0.0"
} }
}, },
"asn1js": {
"version": "2.0.26",
"resolved": "https://registry.npmjs.org/asn1js/-/asn1js-2.0.26.tgz",
"integrity": "sha512-yG89F0j9B4B0MKIcFyWWxnpZPLaNTjCj4tkE3fjbAoo0qmpGw0PYYqSbX/4ebnd9Icn8ZgK4K1fvDyEtW1JYtQ==",
"requires": {
"pvutils": "^1.0.17"
}
},
"assert": { "assert": {
"version": "1.5.0", "version": "1.5.0",
"resolved": "https://registry.npmjs.org/assert/-/assert-1.5.0.tgz", "resolved": "https://registry.npmjs.org/assert/-/assert-1.5.0.tgz",
@ -3662,6 +3733,11 @@
"randomfill": "^1.0.3" "randomfill": "^1.0.3"
} }
}, },
"crypto-random-string": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz",
"integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA=="
},
"css-blank-pseudo": { "css-blank-pseudo": {
"version": "0.1.4", "version": "0.1.4",
"resolved": "https://registry.npmjs.org/css-blank-pseudo/-/css-blank-pseudo-0.1.4.tgz", "resolved": "https://registry.npmjs.org/css-blank-pseudo/-/css-blank-pseudo-0.1.4.tgz",
@ -4277,6 +4353,11 @@
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk="
}, },
"denque": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/denque/-/denque-1.4.1.tgz",
"integrity": "sha512-OfzPuSZKGcgr96rf1oODnfjqBFmr1DVoc/TrItj3Ohe0Ah1C5WX5Baquw/9U9KovnQ88EqmJbD66rKYUQYN1tQ=="
},
"depd": { "depd": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
@ -4662,11 +4743,6 @@
"is-obj": "^2.0.0" "is-obj": "^2.0.0"
} }
}, },
"double-ended-queue": {
"version": "2.1.0-0",
"resolved": "https://registry.npmjs.org/double-ended-queue/-/double-ended-queue-2.1.0-0.tgz",
"integrity": "sha1-ED01J/0xUo9AGIEwyEHv3XgmTlw="
},
"duplexer": { "duplexer": {
"version": "0.1.1", "version": "0.1.1",
"resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz", "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz",
@ -6875,6 +6951,19 @@
"stream-events": "^1.0.4" "stream-events": "^1.0.4"
}, },
"dependencies": { "dependencies": {
"configstore": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/configstore/-/configstore-5.0.1.tgz",
"integrity": "sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA==",
"requires": {
"dot-prop": "^5.2.0",
"graceful-fs": "^4.1.2",
"make-dir": "^3.0.0",
"unique-string": "^2.0.0",
"write-file-atomic": "^3.0.0",
"xdg-basedir": "^4.0.0"
}
},
"gaxios": { "gaxios": {
"version": "2.3.4", "version": "2.3.4",
"resolved": "https://registry.npmjs.org/gaxios/-/gaxios-2.3.4.tgz", "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-2.3.4.tgz",
@ -6886,6 +6975,30 @@
"is-stream": "^2.0.0", "is-stream": "^2.0.0",
"node-fetch": "^2.3.0" "node-fetch": "^2.3.0"
} }
},
"make-dir": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
"integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==",
"requires": {
"semver": "^6.0.0"
}
},
"semver": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw=="
},
"write-file-atomic": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz",
"integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==",
"requires": {
"imurmurhash": "^0.1.4",
"is-typedarray": "^1.0.0",
"signal-exit": "^3.0.2",
"typedarray-to-buffer": "^3.1.5"
}
} }
} }
}, },
@ -7109,8 +7222,7 @@
"graceful-fs": { "graceful-fs": {
"version": "4.2.4", "version": "4.2.4",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz",
"integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==", "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw=="
"dev": true
}, },
"growl": { "growl": {
"version": "1.10.5", "version": "1.10.5",
@ -7994,8 +8106,7 @@
"imurmurhash": { "imurmurhash": {
"version": "0.1.4", "version": "0.1.4",
"resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
"integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o="
"dev": true
}, },
"indent-string": { "indent-string": {
"version": "4.0.0", "version": "4.0.0",
@ -13030,6 +13141,19 @@
"yargs": "^14.0.0" "yargs": "^14.0.0"
} }
}, },
"pvtsutils": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.0.10.tgz",
"integrity": "sha512-8ZKQcxnZKTn+fpDh7wL4yKax5fdl3UJzT8Jv49djZpB/dzPxacyN1Sez90b6YLdOmvIr9vaySJ5gw4aUA1EdSw==",
"requires": {
"tslib": "^1.10.0"
}
},
"pvutils": {
"version": "1.0.17",
"resolved": "https://registry.npmjs.org/pvutils/-/pvutils-1.0.17.tgz",
"integrity": "sha512-wLHYUQxWaXVQvKnwIDWFVKDJku9XDCvyhhxoq8dc5MFdIlRenyPI9eSfEtcvgHgD7FlvCyGAlWgOzRnZD99GZQ=="
},
"q": { "q": {
"version": "1.5.1", "version": "1.5.1",
"resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz",
@ -13361,13 +13485,14 @@
} }
}, },
"redis": { "redis": {
"version": "2.8.0", "version": "3.0.2",
"resolved": "https://registry.npmjs.org/redis/-/redis-2.8.0.tgz", "resolved": "https://registry.npmjs.org/redis/-/redis-3.0.2.tgz",
"integrity": "sha512-M1OkonEQwtRmZv4tEWF2VgpG0JWJ8Fv1PhlgT5+B+uNq2cA3Rt1Yt/ryoR+vQNOQcIEgdCdfH0jr3bDpihAw1A==", "integrity": "sha512-PNhLCrjU6vKVuMOyFu7oSP296mwBkcE6lrAjruBYG5LgdSqtRBoVQIylrMyVZD/lkF24RSNNatzvYag6HRBHjQ==",
"requires": { "requires": {
"double-ended-queue": "^2.1.0-0", "denque": "^1.4.1",
"redis-commands": "^1.2.0", "redis-commands": "^1.5.0",
"redis-parser": "^2.6.0" "redis-errors": "^1.2.0",
"redis-parser": "^3.0.0"
} }
}, },
"redis-commands": { "redis-commands": {
@ -13375,6 +13500,11 @@
"resolved": "https://registry.npmjs.org/redis-commands/-/redis-commands-1.5.0.tgz", "resolved": "https://registry.npmjs.org/redis-commands/-/redis-commands-1.5.0.tgz",
"integrity": "sha512-6KxamqpZ468MeQC3bkWmCB1fp56XL64D4Kf0zJSwDZbVLLm7KFkoIcHrgRvQ+sk8dnhySs7+yBg94yIkAK7aJg==" "integrity": "sha512-6KxamqpZ468MeQC3bkWmCB1fp56XL64D4Kf0zJSwDZbVLLm7KFkoIcHrgRvQ+sk8dnhySs7+yBg94yIkAK7aJg=="
}, },
"redis-errors": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz",
"integrity": "sha1-62LSrbFeTq9GEMBK/hUpOEJQq60="
},
"redis-mock": { "redis-mock": {
"version": "0.47.0", "version": "0.47.0",
"resolved": "https://registry.npmjs.org/redis-mock/-/redis-mock-0.47.0.tgz", "resolved": "https://registry.npmjs.org/redis-mock/-/redis-mock-0.47.0.tgz",
@ -13382,9 +13512,12 @@
"dev": true "dev": true
}, },
"redis-parser": { "redis-parser": {
"version": "2.6.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-2.6.0.tgz", "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz",
"integrity": "sha1-Uu0J2srBCPGmMcB+m2mUHnoZUEs=" "integrity": "sha1-tm2CjNyv5rS4pCin3vTGvKwxyLQ=",
"requires": {
"redis-errors": "^1.0.0"
}
}, },
"reduce-css-calc": { "reduce-css-calc": {
"version": "2.1.7", "version": "2.1.7",
@ -14143,8 +14276,7 @@
"signal-exit": { "signal-exit": {
"version": "3.0.3", "version": "3.0.3",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz",
"integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==", "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA=="
"dev": true
}, },
"simple-swizzle": { "simple-swizzle": {
"version": "0.2.2", "version": "0.2.2",
@ -15885,6 +16017,14 @@
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
"integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c="
}, },
"typedarray-to-buffer": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz",
"integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==",
"requires": {
"is-typedarray": "^1.0.0"
}
},
"ua-parser-js": { "ua-parser-js": {
"version": "0.7.21", "version": "0.7.21",
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.21.tgz", "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.21.tgz",
@ -16025,6 +16165,14 @@
"imurmurhash": "^0.1.4" "imurmurhash": "^0.1.4"
} }
}, },
"unique-string": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz",
"integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==",
"requires": {
"crypto-random-string": "^2.0.0"
}
},
"unist-util-find-all-after": { "unist-util-find-all-after": {
"version": "1.0.5", "version": "1.0.5",
"resolved": "https://registry.npmjs.org/unist-util-find-all-after/-/unist-util-find-all-after-1.0.5.tgz", "resolved": "https://registry.npmjs.org/unist-util-find-all-after/-/unist-util-find-all-after-1.0.5.tgz",

View File

@ -23,11 +23,11 @@
"release": "npm-run-all contributors changelog", "release": "npm-run-all contributors changelog",
"test": "npm-run-all test:*", "test": "npm-run-all test:*",
"test:backend": "nyc --reporter=lcovonly mocha --reporter=min test/backend", "test:backend": "nyc --reporter=lcovonly mocha --reporter=min test/backend",
"test:frontend": "cross-env NODE_ENV=development node test/frontend/runner.js", "test:frontend": "cross-env NODE_ENV=development FXA_REQUIRED=false node test/frontend/runner.js",
"test:report": "nyc report --reporter=html", "test:report": "nyc report --reporter=html",
"test-integration": "cross-env NODE_ENV=development wdio test/wdio.docker.conf.js", "test-integration": "cross-env NODE_ENV=development wdio test/wdio.docker.conf.js",
"circleci-test-integration": "echo 'webdriverio tests need to be updated to node 12'", "circleci-test-integration": "echo 'webdriverio tests need to be updated to node 12'",
"start": "npm run clean && cross-env NODE_ENV=development L10N_DEV=true FXA_CLIENT_ID=fced6b5e3f4c66b9 BASE_URL=http://localhost:8080 webpack-dev-server --mode=development", "start": "npm run clean && cross-env NODE_ENV=development L10N_DEV=true FXA_CLIENT_ID=fced6b5e3f4c66b9 BASE_URL=http://localhost:1337 webpack-dev-server --port=1337 --mode=development",
"android": "cross-env ANDROID=1 npm start", "android": "cross-env ANDROID=1 npm start",
"prod": "node server/bin/prod.js" "prod": "node server/bin/prod.js"
}, },
@ -134,6 +134,7 @@
"@fluent/bundle": "^0.13.0", "@fluent/bundle": "^0.13.0",
"@fluent/langneg": "^0.3.0", "@fluent/langneg": "^0.3.0",
"@google-cloud/storage": "^4.1.1", "@google-cloud/storage": "^4.1.1",
"@peculiar/webcrypto": "^1.1.1",
"@sentry/node": "^5.8.0", "@sentry/node": "^5.8.0",
"aws-sdk": "^2.568.0", "aws-sdk": "^2.568.0",
"body-parser": "^1.19.0", "body-parser": "^1.19.0",
@ -147,7 +148,7 @@
"mkdirp": "^0.5.1", "mkdirp": "^0.5.1",
"mozlog": "^2.2.0", "mozlog": "^2.2.0",
"node-fetch": "^2.6.0", "node-fetch": "^2.6.0",
"redis": "^2.8.0", "redis": "^3.0.2",
"selenium-standalone": "^6.15.6", "selenium-standalone": "^6.15.6",
"ua-parser-js": "^0.7.20" "ua-parser-js": "^0.7.20"
}, },

View File

@ -1,6 +1,5 @@
# Firefox Send is a brand name and should not be localized. # Firefox Send is a brand name and should not be localized.
title = Firefox Send title = Firefox Send
siteFeedback = Feedback
importingFile = Importing… importingFile = Importing…
encryptingFile = Encrypting… encryptingFile = Encrypting…
decryptingFile = Decrypting… decryptingFile = Decrypting…
@ -109,6 +108,7 @@ legalDateStamp = Version 1.0, dated March 12, 2019
# A short representation of a countdown timer containing the number of days, hours, and minutes remaining as digits, example "2d 11h 56m" # 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 expiresDaysHoursMinutes = { $days }d { $hours }h { $minutes }m
addFilesButton = Select files to upload addFilesButton = Select files to upload
trustWarningMessage = Make sure you trust your recipient when sharing sensitive data.
uploadButton = Upload uploadButton = Upload
# the first part of the string 'Drag and drop files or click to send up to 1GB' # the first part of the string 'Drag and drop files or click to send up to 1GB'
dragAndDropFiles = Drag and drop files dragAndDropFiles = Drag and drop files
@ -145,3 +145,33 @@ shareLinkButton = Share link
shareMessage = Download “{ $name }” with { -send-brand }: simple, safe file sharing shareMessage = Download “{ $name }” with { -send-brand }: simple, safe file sharing
trailheadPromo = There is a way to protect your privacy. Join Firefox. trailheadPromo = There is a way to protect your privacy. Join Firefox.
learnMore = Learn more. learnMore = Learn more.
downloadFlagged = This link has been disabled for violating the terms of service.
downloadConfirmTitle = One more thing
downloadConfirmDescription = Make sure you trust the person who sent you this file because we cant verify that it will not harm your device.
# 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] I trust the person who sent this file
*[other] I trust the person who sent these files
}
# 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] Report this file as suspicious
*[other] Report these files as suspicious
}
reportDescription = Help us understand whats going on. What do you think is wrong with these files?
reportUnknownDescription = Please go to the url of the link you wish to report and click “{ reportFile }”.
reportButton = Report
reportReasonMalware = These files contain malware or are part of a phishing attack.
reportReasonPii = These files contain personally identifiable information about me.
reportReasonAbuse = These files contain illegal or abusive content.
reportReasonCopyright = To report copyright or trademark infringement, use the process described at <a>this page</a>.
reportedTitle = Files Reported
reportedDescription = Thank you. We have received your report on these files.

View File

@ -96,6 +96,28 @@ function statDeleteEvent(data) {
return sendBatch([event]); return sendBatch([event]);
} }
function statReportEvent(data) {
const loc = location(data.ip);
const event = {
session_id: -1,
country: loc.country,
region: loc.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, ip) { function clientEvent(event, ua, language, session_id, deltaT, platform, ip) {
const loc = location(ip); const loc = location(ip);
const ep = event.event_properties || {}; const ep = event.event_properties || {};
@ -173,6 +195,7 @@ module.exports = {
statUploadEvent, statUploadEvent,
statDownloadEvent, statDownloadEvent,
statDeleteEvent, statDeleteEvent,
statReportEvent,
clientEvent, clientEvent,
sendBatch sendBatch
}; };

View File

@ -14,7 +14,7 @@ module.exports = function(app, devServer) {
expressWs(wsapp, null, { perMessageDeflate: false }); expressWs(wsapp, null, { perMessageDeflate: false });
routes(wsapp); routes(wsapp);
wsapp.ws('/api/ws', require('../routes/ws')); wsapp.ws('/api/ws', require('../routes/ws'));
wsapp.listen(8081, config.listen_address); wsapp.listen(1338, config.listen_address);
assets.setMiddleware(devServer.middleware); assets.setMiddleware(devServer.middleware);
app.use(morgan('dev', { stream: process.stderr })); app.use(morgan('dev', { stream: process.stderr }));

View File

@ -120,6 +120,11 @@ const conf = convict({
default: '', default: '',
env: 'SENTRY_DSN' env: 'SENTRY_DSN'
}, },
sentry_host: {
format: String,
default: 'https://sentry.prod.mozaws.net',
env: 'SENTRY_HOST'
},
env: { env: {
format: ['production', 'development', 'test'], format: ['production', 'development', 'test'],
default: 'development', default: 'development',
@ -150,9 +155,14 @@ const conf = convict({
default: `${tmpdir()}${path.sep}send-${randomBytes(4).toString('hex')}`, default: `${tmpdir()}${path.sep}send-${randomBytes(4).toString('hex')}`,
env: 'FILE_DIR' env: 'FILE_DIR'
}, },
fxa_required: {
format: Boolean,
default: true,
env: 'FXA_REQUIRED'
},
fxa_url: { fxa_url: {
format: 'url', format: 'url',
default: 'https://send-fxa.dev.lcip.org', default: 'http://localhost:3030',
env: 'FXA_URL' env: 'FXA_URL'
}, },
fxa_client_id: { fxa_client_id: {

53
server/keychain.js Normal file
View File

@ -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));
}
};

View File

@ -7,6 +7,9 @@ class Metadata {
this.metadata = obj.metadata; this.metadata = obj.metadata;
this.auth = obj.auth; this.auth = obj.auth;
this.nonce = obj.nonce; this.nonce = obj.nonce;
this.flagged = !!obj.flagged;
this.dead = !!obj.dead;
this.key = obj.key;
} }
} }

View File

@ -46,7 +46,7 @@ module.exports = {
if (id && ownerToken) { if (id && ownerToken) {
try { try {
req.meta = await storage.metadata(id); req.meta = await storage.metadata(id);
if (!req.meta) { if (!req.meta || req.meta.dead) {
return res.sendStatus(404); return res.sendStatus(404);
} }
const metaOwner = Buffer.from(req.meta.owner, 'utf8'); const metaOwner = Buffer.from(req.meta.owner, 'utf8');

View File

@ -6,7 +6,7 @@ module.exports = async function(req, res) {
const id = req.params.id; const id = req.params.id;
const meta = req.meta; const meta = req.meta;
const ttl = await storage.ttl(id); const ttl = await storage.ttl(id);
await storage.del(id); await storage.kill(id);
res.sendStatus(200); res.sendStatus(200);
statDeleteEvent({ statDeleteEvent({
id, id,

View File

@ -7,6 +7,9 @@ module.exports = async function(req, res) {
const id = req.params.id; const id = req.params.id;
try { try {
const meta = req.meta; const meta = req.meta;
if (meta.dead || meta.flagged) {
return res.sendStatus(404);
}
const fileStream = await storage.get(id); const fileStream = await storage.get(id);
let cancelled = false; let cancelled = false;
@ -33,7 +36,7 @@ module.exports = async function(req, res) {
}); });
try { try {
if (dl >= dlimit) { if (dl >= dlimit) {
await storage.del(id); await storage.kill(id);
} else { } else {
await storage.incrementField(id, 'dl'); await storage.incrementField(id, 'dl');
} }

View File

@ -3,6 +3,9 @@ const storage = require('../storage');
module.exports = async (req, res) => { module.exports = async (req, res) => {
try { try {
const meta = await storage.metadata(req.params.id); 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.set('WWW-Authenticate', `send-v1 ${meta.nonce}`);
res.send({ res.send({
requiresPassword: meta.pwd requiresPassword: meta.pwd

View File

@ -32,55 +32,57 @@ module.exports = function(app) {
}); });
if (!IS_DEV) { if (!IS_DEV) {
let csp = { let csp = {
directives: { directives: {
defaultSrc: ["'self'"], defaultSrc: ["'self'"],
connectSrc: [ connectSrc: [
"'self'", "'self'",
'wss://*.dev.lcip.org', config.base_url.replace(/^https:\/\//, 'wss://')
'wss://*.send.nonprod.cloudops.mozgcp.net', ],
config.base_url.replace(/^https:\/\//, 'wss://'), imgSrc: ["'self'"],
'https://*.dev.lcip.org', scriptSrc: [
'https://accounts.firefox.com', "'self'",
'https://*.accounts.firefox.com', function(req) {
'https://sentry.prod.mozaws.net' return `'nonce-${req.cspNonce}'`;
], }
imgSrc: [ ],
"'self'", formAction: ["'none'"],
'https://*.dev.lcip.org', frameAncestors: ["'none'"],
'https://firefoxusercontent.com', objectSrc: ["'none'"],
'https://secure.gravatar.com' reportUri: '/__cspreport__'
],
scriptSrc: [
"'self'",
function(req) {
return `'nonce-${req.cspNonce}'`;
}
],
formAction: ["'none'"],
frameAncestors: ["'none'"],
objectSrc: ["'none'"],
reportUri: '/__cspreport__'
}
} }
};
csp.directives.connectSrc.push(config.base_url.replace(/^https:\/\//,'wss://')) if (config.fxa_client_id) {
if(config.fxa_csp_oauth_url != ""){ csp.directives.connectSrc.push('https://accounts.firefox.com');
csp.directives.connectSrc.push(config.fxa_csp_oauth_url) 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.fxa_csp_content_url != "" ){ if (config.sentry_id) {
csp.directives.connectSrc.push(config.fxa_csp_content_url) csp.directives.connectSrc.push(config.sentry_host);
} }
if(config.fxa_csp_profile_url != "" ){ if (
csp.directives.connectSrc.push(config.fxa_csp_profile_url) config.base_url.test(/^https:\/\/.*\.dev\.lcip\.org$/) ||
config.base_url.test(
/^https:\/\/.*\.send\.nonprod\.cloudops\.mozgcp\.net$/
)
) {
csp.directives.connectSrc.push('https://*.dev.lcip.org');
csp.directives.imgSrc.push('https://*.dev.lcip.org');
} }
if(config.fxa_csp_profileimage_url != ""){ if (config.fxa_csp_oauth_url != '') {
csp.directives.imgSrc.push(config.fxa_csp_profileimage_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(
helmet.contentSecurityPolicy(csp)
);
} }
app.use(function(req, res, next) { app.use(function(req, res, next) {
@ -101,6 +103,7 @@ module.exports = function(app) {
app.get('/oauth', language, pages.blank); app.get('/oauth', language, pages.blank);
app.get('/legal', language, pages.legal); app.get('/legal', language, pages.legal);
app.get('/login', language, pages.index); app.get('/login', language, pages.index);
app.get('/report', language, pages.blank);
app.get('/app.webmanifest', language, require('./webmanifest')); app.get('/app.webmanifest', language, require('./webmanifest'));
app.get(`/download/:id${ID_REGEX}`, language, pages.download); app.get(`/download/:id${ID_REGEX}`, language, pages.download);
app.get('/unsupported/:reason', language, pages.unsupported); app.get('/unsupported/:reason', language, pages.unsupported);
@ -114,7 +117,7 @@ module.exports = function(app) {
app.get(`/api/metadata/:id${ID_REGEX}`, auth.hmac, require('./metadata')); app.get(`/api/metadata/:id${ID_REGEX}`, auth.hmac, require('./metadata'));
app.get('/api/filelist/:id([\\w-]{16})', auth.fxa, filelist.get); 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/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/delete/:id${ID_REGEX}`, auth.owner, require('./delete'));
app.post(`/api/password/:id${ID_REGEX}`, auth.owner, require('./password')); app.post(`/api/password/:id${ID_REGEX}`, auth.owner, require('./password'));
app.post( app.post(
@ -124,6 +127,7 @@ module.exports = function(app) {
require('./params') require('./params')
); );
app.post(`/api/info/:id${ID_REGEX}`, auth.owner, require('./info')); app.post(`/api/info/:id${ID_REGEX}`, auth.owner, require('./info'));
app.post(`/api/report/:id${ID_REGEX}`, require('./report'));
app.post('/api/metrics', require('./metrics')); app.post('/api/metrics', require('./metrics'));
app.get('/__version__', function(req, res) { app.get('/__version__', function(req, res) {
// eslint-disable-next-line node/no-missing-require // eslint-disable-next-line node/no-missing-require

View File

@ -4,9 +4,13 @@ module.exports = async function(req, res) {
const id = req.params.id; const id = req.params.id;
const meta = req.meta; const meta = req.meta;
try { try {
if (meta.dead && !meta.flagged) {
return res.sendStatus(404);
}
const ttl = await storage.ttl(id); const ttl = await storage.ttl(id);
res.send({ res.send({
metadata: meta.metadata, metadata: meta.metadata,
flagged: !!meta.flagged,
finalDownload: meta.dl + 1 === meta.dlimit, finalDownload: meta.dl + 1 === meta.dlimit,
ttl ttl
}); });

View File

@ -23,14 +23,17 @@ module.exports = {
const id = req.params.id; const id = req.params.id;
const appState = await state(req); const appState = await state(req);
try { 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.set('WWW-Authenticate', `send-v1 ${nonce}`);
res.send( res.send(
stripEvents( stripEvents(
routes().toString( routes().toString(
`/download/${id}`, `/download/${id}`,
Object.assign(appState, { Object.assign(appState, {
downloadMetadata: { nonce, pwd } downloadMetadata: { nonce, pwd, flagged }
}) })
) )
) )

39
server/routes/report.js Normal file
View File

@ -0,0 +1,39 @@
const storage = require('../storage');
const Keychain = require('../keychain');
const { statReportEvent } = require('../amplitude');
module.exports = async function(req, res) {
try {
const id = req.params.id;
const meta = await storage.metadata(id);
if (meta.flagged) {
return res.sendStatus(200);
}
try {
const key = req.body.key;
const keychain = new Keychain(key);
const metadata = await keychain.decryptMetadata(
Buffer.from(meta.metadata, 'base64')
);
if (metadata.manifest) {
storage.flag(id, key);
statReportEvent({
id,
ip: req.ip,
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)
});
return res.sendStatus(200);
}
res.sendStatus(400);
} catch (e) {
console.error(e);
res.sendStatus(400);
}
} catch (e) {
res.sendStatus(404);
}
};

View File

@ -46,7 +46,8 @@ module.exports = function(ws, req) {
!auth || !auth ||
timeLimit <= 0 || timeLimit <= 0 ||
timeLimit > maxExpireSeconds || timeLimit > maxExpireSeconds ||
dlimit > maxDownloads dlimit > maxDownloads ||
(config.fxa_required && !user)
) { ) {
ws.send( ws.send(
JSON.stringify({ JSON.stringify({

View File

@ -15,7 +15,11 @@ module.exports = async function(req) {
try { try {
authConfig = await getFxaConfig(); authConfig = await getFxaConfig();
authConfig.client_id = config.fxa_client_id; authConfig.client_id = config.fxa_client_id;
authConfig.fxa_required = config.fxa_required;
} catch (e) { } catch (e) {
if (config.auth_required) {
throw new Error('fxa_required is set but no config was found');
}
// continue without accounts // continue without accounts
} }
} }

View File

@ -33,7 +33,15 @@ class DB {
} }
async getPrefixedId(id) { async getPrefixedId(id) {
const prefix = await this.redis.hgetAsync(id, 'prefix'); const [prefix, dead, flagged] = await this.redis.hmgetAsync(
id,
'prefix',
'dead',
'flagged'
);
if (dead || flagged) {
throw new Error('id not available');
}
return `${prefix}-${id}`; return `${prefix}-${id}`;
} }
@ -51,9 +59,10 @@ class DB {
const prefix = getPrefix(expireSeconds); const prefix = getPrefix(expireSeconds);
const filePath = `${prefix}-${id}`; const filePath = `${prefix}-${id}`;
await this.storage.set(filePath, file); await this.storage.set(filePath, file);
this.redis.hset(id, 'prefix', prefix);
if (meta) { 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); this.redis.expire(id, expireSeconds);
} }
@ -66,6 +75,16 @@ class DB {
this.redis.hincrby(id, key, increment); this.redis.hincrby(id, key, increment);
} }
kill(id) {
this.redis.hset(id, 'dead', 1);
}
async flag(id, key) {
// this.redis.persist(id);
this.redis.hmset(id, { flagged: 1, key });
this.redis.sadd('flagged', id);
}
async del(id) { async del(id) {
const filePath = await this.getPrefixedId(id); const filePath = await this.getPrefixedId(id);
this.storage.del(filePath); this.storage.del(filePath);

View File

@ -23,6 +23,8 @@ module.exports = function(config) {
client.ttlAsync = promisify(client.ttl); client.ttlAsync = promisify(client.ttl);
client.hgetallAsync = promisify(client.hgetall); client.hgetallAsync = promisify(client.hgetall);
client.hgetAsync = promisify(client.hget); client.hgetAsync = promisify(client.hget);
client.hmgetAsync = promisify(client.hmget);
client.pingAsync = promisify(client.ping); client.pingAsync = promisify(client.ping);
client.existsAsync = promisify(client.exists);
return client; return client;
}; };

View File

@ -259,6 +259,14 @@ module.exports = {
full: '100%', full: '100%',
screen: '100vh' 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: { minWidth: {
'0': '0', '0': '0',
full: '100%' full: '100%'

View File

@ -2,7 +2,7 @@ const sinon = require('sinon');
const proxyquire = require('proxyquire').noCallThru(); const proxyquire = require('proxyquire').noCallThru();
const storage = { const storage = {
del: sinon.stub(), kill: sinon.stub(),
ttl: sinon.stub() ttl: sinon.stub()
}; };
@ -24,19 +24,19 @@ const delRoute = proxyquire('../../server/routes/delete', {
describe('/api/delete', function() { describe('/api/delete', function() {
afterEach(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 req = request('x');
const res = response(); const res = response();
await delRoute(req, res); await delRoute(req, res);
sinon.assert.calledWith(storage.del, 'x'); sinon.assert.calledWith(storage.kill, 'x');
sinon.assert.calledWith(res.sendStatus, 200); sinon.assert.calledWith(res.sendStatus, 200);
}); });
it('sends a 404 on failure', async function() { 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(); const res = response();
await delRoute(request('x'), res); await delRoute(request('x'), res);
sinon.assert.calledWith(res.sendStatus, 404); sinon.assert.calledWith(res.sendStatus, 404);

View File

@ -6,7 +6,7 @@ const storage = {
length: sinon.stub() length: sinon.stub()
}; };
function request(id, meta) { function request(id, meta = {}) {
return { return {
params: { id }, params: { id },
meta meta

View File

@ -133,7 +133,12 @@ describe('Storage', function() {
}; };
await storage.set('x', null, m); await storage.set('x', null, m);
const meta = await storage.metadata('x'); const meta = await storage.metadata('x');
assert.deepEqual(meta, m); assert.deepEqual(meta, {
...m,
dead: false,
flagged: false,
key: undefined
});
}); });
}); });
}); });

View File

@ -207,7 +207,7 @@ const web = {
host: '0.0.0.0', host: '0.0.0.0',
proxy: { proxy: {
'/api/ws': { '/api/ws': {
target: 'ws://localhost:8081', target: 'ws://localhost:1338',
ws: true, ws: true,
secure: false secure: false
} }