Compare commits

...

37 Commits

Author SHA1 Message Date
timvisee
6dafdcdebd Bump version to 3.4.5 2021-03-15 21:17:44 +01:00
timvisee
dc03b42b96 Update dependencies 2021-03-15 21:16:57 +01:00
Tim Visée
3e07f648b3 Merge branch 'remove-fxa-dialog-on-big-file' into 'master'
Remove FxA dialog on file too big error

See merge request timvisee/send!12
2021-03-15 20:15:27 +00:00
Romain Hv
f58597cece Remove FxA dialog on file too big error 2021-03-15 20:58:47 +01:00
Tim Visée
d3f9b82672 Merge branch 'remove-metrics' into 'master'
Remove metrics

Closes #4

See merge request timvisee/send!11
2021-03-15 19:49:26 +00:00
Romain Hv
a0bc20aeb6 Remove metrics #4 2021-03-15 19:56:51 +01:00
timvisee
d03e83dd66 Merge branch 'dependabot/npm_and_yarn/elliptic-6.5.4'
Fixes https://github.com/timvisee/send/pull/8
2021-03-11 16:22:08 +01:00
dependabot[bot]
94e80ccee9 Bump elliptic from 6.5.3 to 6.5.4
Bumps [elliptic](https://github.com/indutny/elliptic) from 6.5.3 to 6.5.4.
- [Release notes](https://github.com/indutny/elliptic/releases)
- [Commits](https://github.com/indutny/elliptic/compare/v6.5.3...v6.5.4)

Signed-off-by: dependabot[bot] <support@github.com>
2021-03-11 16:21:38 +01:00
Tim Visée
f8358c4dac Merge branch 'help-review' into 'master'
Improvement of the reverse proxy guidelines, the given configuration example...

See merge request timvisee/send!10
2021-03-07 13:27:11 +00:00
Florian HEGRON
ec3cff63a2 Improvement of the reverse proxy guidelines, the given configuration example requires to have the apache rewrite mod enabled. 2021-03-07 14:24:47 +01:00
Tim Visée
8f192482b5 Merge branch 'help-review' into 'master'
Delete dead links about testing environments in the README.md

See merge request timvisee/send!9
2021-03-07 09:07:17 +00:00
Florian HEGRON
808a04b669 Delete dead links about testing environments in the README.md 2021-03-07 09:03:20 +00:00
timvisee
71a925a674 Bump version to 3.4.4 2021-02-16 15:18:03 +01:00
timvisee
64d9cd694d Update dependencies 2021-02-16 15:15:28 +01:00
timvisee
94b78b425f Use node 15 Docker image on GitLab CI 2021-02-16 15:12:23 +01:00
timvisee
42e94139a2 Merge branch 'atomheartother:master' into 'master'
See https://github.com/timvisee/send/pull/6
2021-02-16 15:10:22 +01:00
E. Navennec
6bd6280fb5 Use up to date alpine images and not slim images 2021-02-16 14:56:39 +01:00
timvisee
1f2c524b40 Include redis-mock in main build to allow usage without Redis 2021-02-16 14:56:00 +01:00
timvisee
854810c242 Use short commit hashes in Docker image tags 2021-02-16 14:24:58 +01:00
timvisee
45024d3dc6 Rework GitLab CI configuration, always build Docker image artifact
This artifact will be used as master-branch and release image, without
building the image again
2021-02-16 14:12:49 +01:00
timvisee
0806b8fd9d Bump version to 3.4.3 2021-02-05 02:31:15 +01:00
timvisee
2dbc740998 Update dependencies 2021-02-05 02:29:19 +01:00
timvisee
5b9c8301c7 Fix incorrect environment variable for config property 2021-02-05 02:24:48 +01:00
timvisee
42506dda9d Bump version to 3.4.2 2021-01-27 13:38:58 +01:00
timvisee
8e868a642c Fix footer, properly layout no affiliation notice on small screens 2021-01-27 13:34:26 +01:00
timvisee
638f68334a Bump version to 3.4.1 2021-01-27 00:35:26 +01:00
timvisee
d2907c6d8b Add missing global variable to linter configuration to fix lint test 2021-01-27 00:30:44 +01:00
timvisee
4b05a2f705 Update dependencies 2021-01-27 00:25:04 +01:00
timvisee
6960cc75fa Add configurable donate, CLI and DMCA and source links in footer 2021-01-27 00:21:03 +01:00
timvisee
7f3da34318 Update Docker configuration 2021-01-26 20:22:04 +01:00
timvisee
4369baa258 Add S3 lifecycle policy example configuration 2021-01-26 20:14:09 +01:00
timvisee
6f1942a446 Add clients section in README 2021-01-24 21:38:05 +01:00
timvisee
b6d2e7c1ca Update dependencies 2021-01-18 16:04:11 +01:00
timvisee
58dd5b7a70 Merge branch 'dependabot/npm_and_yarn/ini-1.3.8' 2021-01-10 23:01:20 +01:00
timvisee
4f3a2e4fc1 Merge branch 'dependabot/npm_and_yarn/ini-1.3.8' 2021-01-10 23:00:03 +01:00
timvisee
a798b14620 Merge branch 'master' into dependabot/npm_and_yarn/ini-1.3.8 2021-01-10 22:59:42 +01:00
dependabot[bot]
2bc1a13ae6 Bump ini from 1.3.5 to 1.3.8
Bumps [ini](https://github.com/isaacs/ini) from 1.3.5 to 1.3.8.
- [Release notes](https://github.com/isaacs/ini/releases)
- [Commits](https://github.com/isaacs/ini/compare/v1.3.5...v1.3.8)

Signed-off-by: dependabot[bot] <support@github.com>
2021-01-09 18:20:00 +00:00
34 changed files with 1809 additions and 2016 deletions

View File

@@ -1,4 +1,4 @@
image: "node:12-slim"
image: "node:15-slim"
stages:
- test
@@ -17,6 +17,7 @@ before_script:
- apt-get update
- apt-get install -y google-chrome-stable fonts-ipafont-gothic fonts-wqy-zenhei fonts-thai-tlwg fonts-kacst fonts-freefont-ttf libxss1 --no-install-recommends
# Build Send, run npm tests
test:
stage: test
script:
@@ -24,48 +25,81 @@ test:
- npm run lint
- npm test
# Release Docker image artifact for easy testing
# Build Docker image, export Docker image artifact
artifact-docker:
stage: artifact
image: docker:latest
needs: []
services:
- docker:dind
variables:
IMG_FILE: "send:git-$CI_COMMIT_SHORT_SHA.tar"
IMG_NAME: "send:git-$CI_COMMIT_SHORT_SHA"
before_script: []
script:
- docker build -t $IMG_NAME .
- docker image save -o $IMG_FILE $IMG_NAME
artifacts:
name: artifact-docker
paths:
- $IMG_FILE
expire_in: 1 week
# Release public Docker image for the master branch
release-docker-master:
stage: release
image: docker:latest
dependencies:
- artifact-docker
services:
- docker:dind
only:
- master
variables:
IMG_IMPORT_FILE: "send:git-$CI_COMMIT_SHORT_SHA.tar"
IMG_IMPORT_NAME: "send:git-$CI_COMMIT_SHORT_SHA"
IMG_NAME: "registry.gitlab.com/timvisee/send:master-$CI_COMMIT_SHORT_SHA"
before_script: []
script:
- export IMG_NAME=registry.gitlab.com/timvisee/send:master-$CI_COMMIT_SHA
# Login in to registry
- 'docker login registry.gitlab.com -u $DOCKER_USER -p $DOCKER_PASS'
# Build and push image, report image name
- docker build -t $IMG_NAME .
- docker push $IMG_NAME
- 'echo Docker image artifact published, available as:'
- 'echo " docker pull $IMG_NAME"'
# Load existing, retag for new image images
- docker image load -i $IMG_IMPORT_FILE
- docker tag $IMG_IMPORT_NAME $IMG_NAME
# Release public Docker image
# Publish tagged image
- docker push $IMG_NAME
- 'echo "Docker image artifact published, available as:" && echo " docker pull $IMG_NAME"'
# Release public Docker image for a version tag
release-docker:
stage: release
image: docker:latest
dependencies:
- artifact-docker
services:
- docker:dind
only:
- /^v(\d+\.)*\d+$/
variables:
IMG_IMPORT_FILE: "send:git-$CI_COMMIT_SHORT_SHA.tar"
IMG_IMPORT_NAME: "send:git-$CI_COMMIT_SHORT_SHA"
IMG_NAME: "registry.gitlab.com/timvisee/send:$CI_COMMIT_REF_NAME"
IMG_NAME_LATEST: "registry.gitlab.com/timvisee/send:latest"
before_script: []
script:
- export IMG_NAME=registry.gitlab.com/timvisee/send:$CI_COMMIT_REF_NAME
- export IMG_NAME_LATEST=registry.gitlab.com/timvisee/send:latest
# Login in to registry
- 'docker login registry.gitlab.com -u $DOCKER_USER -p $DOCKER_PASS'
# Build and push image, report image name
- docker build -t $IMG_NAME .
- docker tag $IMG_NAME $IMG_NAME_LATEST
# Load existing, retag for new image images
- docker image load -i $IMG_IMPORT_FILE
- docker tag $IMG_IMPORT_NAME $IMG_NAME
- docker tag $IMG_IMPORT_NAME $IMG_NAME_LATEST
# Publish tagged image
- docker push $IMG_NAME
- docker push $IMG_NAME_LATEST
- 'echo Docker image artifact published, available as:'
- 'echo " docker pull $IMG_NAME_LATEST"'
- 'echo " docker pull $IMG_NAME"'
- 'echo "Docker image artifact published, available as:" && echo " docker pull $IMG_NAME_LATEST" && echo " docker pull $IMG_NAME"'

View File

@@ -6,13 +6,13 @@
# Build project
FROM node:12 AS builder
FROM node:15.5.1-alpine AS builder
RUN set -x \
# Add user
&& addgroup --gid 10001 app \
&& adduser --disabled-password \
--gecos '' \
--gid 10001 \
--ingroup app \
--home /app \
--uid 10001 \
app
@@ -26,19 +26,17 @@ RUN set -x \
# Main image
FROM node:12-slim
FROM node:15.5.1-alpine
RUN set -x \
# Add user
&& addgroup --gid 10001 app \
&& adduser --disabled-password \
--gecos '' \
--gid 10001 \
--ingroup app \
--home /app \
--uid 10001 \
app
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 ./

View File

@@ -53,7 +53,7 @@ Thanks [Mozilla][mozilla] for building this amazing tool!
---
**Docs:** [FAQ](docs/faq.md), [Encryption](docs/encryption.md), [Build](docs/build.md), [Docker](docs/docker.md), [Metrics](docs/metrics.md), [More](docs/)
**Docs:** [FAQ](docs/faq.md), [Encryption](docs/encryption.md), [Build](docs/build.md), [Docker](docs/docker.md), [More](docs/)
---
@@ -66,9 +66,8 @@ Thanks [Mozilla][mozilla] for building this amazing tool!
* [Configuration](#configuration)
* [Localization](#localization)
* [Contributing](#contributing)
* [Testing](#testing)
* [Deployment](#deployment)
* [Android](#android)
* [Clients](#clients)
* [License](#license)
---
@@ -133,25 +132,24 @@ Pull requests are always welcome! Feel free to check out the list of ["good firs
---
## Testing
| ENVIRONMENT | URL
|-------------|-----
| Production | <https://send.firefox.com/>
| Stage | <https://stage.send.nonprod.cloudops.mozgcp.net/>
| Development | <https://send2.dev.lcip.org/>
---
## Deployment
see also [docs/deployment.md](docs/deployment.md)
---
## Android
## Clients
The android implementation is contained in the `android` directory, and can be viewed locally for easy testing and editing by running `ANDROID=1 npm start` and then visiting <http://localhost:8080>. CSS and image files are located in the `android/app/src/main/assets` directory.
- Web: _this repository_
- Command-line: [`ffsend`](https://github.com/timvisee/ffsend)
- Android: _see [Android](#android) section_
#### Android
The android implementation is contained in the `android` directory,
and can be viewed locally for easy testing and editing by running `ANDROID=1 npm
start` and then visiting <http://localhost:8080>. CSS and image files are
located in the `android/app/src/main/assets` directory.
---

View File

@@ -4,7 +4,6 @@ import html from 'choo/html';
import * as Sentry from '@sentry/browser';
import { setApiUrlPrefix, getConstants } from '../app/api';
import metrics from '../app/metrics';
//import assets from '../common/assets';
import Archive from '../app/archive';
import Header from '../app/ui/header';
@@ -69,9 +68,10 @@ function body(main) {
(async function start() {
const translate = await getTranslator('en-US');
setTranslate(translate);
const { LIMITS, DEFAULTS } = await getConstants();
const { LIMITS, WEB_UI, DEFAULTS } = await getConstants();
app.use(state => {
state.LIMITS = LIMITS;
state.WEB_UI = WEB_UI;
state.DEFAULTS = DEFAULTS;
state.translate = translate;
state.capabilities = {
@@ -82,7 +82,6 @@ function body(main) {
state.user = new User(storage, LIMITS);
state.sentry = Sentry;
});
app.use(metrics);
app.route('/', body(home));
app.route('/upload', upload);
app.route('/share/:id', share);

View File

@@ -420,17 +420,6 @@ export async function setFileList(bearerToken, kid, data) {
return response.ok;
}
export function sendMetrics(blob) {
if (!navigator.sendBeacon) {
return;
}
try {
navigator.sendBeacon(getApiUrl('/api/metrics'), blob);
} catch (e) {
console.error(e);
}
}
export async function getConstants() {
const response = await fetch(getApiUrl('/config'));

View File

@@ -1,4 +1,3 @@
import * as metrics from './metrics';
import FileReceiver from './fileReceiver';
import FileSender from './fileSender';
import copyDialog from './ui/copyDialog';
@@ -54,7 +53,6 @@ export default function(state, emitter) {
emitter.on('logout', async () => {
await state.user.logout();
metrics.loggedOut({ trigger: 'button' });
emitter.emit('pushState', '/');
});
@@ -68,14 +66,6 @@ export default function(state, emitter) {
emitter.on('delete', async ownedFile => {
try {
metrics.deletedUpload({
size: ownedFile.size,
time: ownedFile.time,
speed: ownedFile.speed,
type: ownedFile.type,
ttl: ownedFile.expiresAt - Date.now(),
location
});
state.storage.remove(ownedFile.id);
await ownedFile.del();
} catch (e) {
@@ -101,9 +91,6 @@ export default function(state, emitter) {
state.LIMITS.MAX_FILES_PER_ARCHIVE
);
} catch (e) {
if (e.message === 'fileTooBig' && maxSize < state.LIMITS.MAX_FILE_SIZE) {
return emitter.emit('signup-cta', 'size');
}
state.modal = okDialog(
state.translate(e.message, {
size: bytes(maxSize),
@@ -123,7 +110,7 @@ export default function(state, emitter) {
source: query.utm_source,
term: query.utm_term
});
state.modal = signupDialog(source);
state.modal = signupDialog();
render();
});
@@ -159,12 +146,9 @@ export default function(state, emitter) {
const links = openLinksInNewTab();
await delay(200);
const start = Date.now();
try {
const ownedFile = await sender.upload(archive, state.user.bearerToken);
state.storage.totalUploads += 1;
const duration = Date.now() - start;
metrics.completedUpload(archive, duration);
faviconProgressbar.updateFavicon(0);
state.storage.addFile(ownedFile);
@@ -181,7 +165,6 @@ export default function(state, emitter) {
} catch (err) {
if (err.message === '0') {
//cancelled. do nothing
metrics.cancelledUpload(archive, err.duration);
render();
} else if (err.message === '401') {
const refreshed = await state.user.refresh();
@@ -197,7 +180,6 @@ export default function(state, emitter) {
scope.setExtra('size', err.size);
state.sentry.captureException(err);
});
metrics.stoppedUpload(archive, err.duration);
emitter.emit('pushState', '/error');
}
} finally {
@@ -249,13 +231,11 @@ export default function(state, emitter) {
render();
});
emitter.on('download', async file => {
emitter.on('download', async () => {
state.transfer.on('progress', updateProgress);
state.transfer.on('decrypting', render);
state.transfer.on('complete', render);
const links = openLinksInNewTab();
const size = file.size;
const start = Date.now();
try {
const dl = state.transfer.download({
stream: state.capabilities.streamDownload
@@ -263,12 +243,6 @@ export default function(state, emitter) {
render();
await dl;
state.storage.totalDownloads += 1;
const duration = Date.now() - start;
metrics.completedDownload({
size,
duration,
password_protected: file.requiresPassword
});
faviconProgressbar.updateFavicon(0);
} catch (err) {
if (err.message === '0') {
@@ -286,12 +260,6 @@ export default function(state, emitter) {
scope.setExtra('progress', err.progress);
state.sentry.captureException(err);
});
const duration = Date.now() - start;
metrics.stoppedDownload({
size,
duration,
password_protected: file.requiresPassword
});
}
emitter.emit('pushState', location);
}
@@ -302,7 +270,6 @@ export default function(state, emitter) {
emitter.on('copy', ({ url }) => {
copyToClipboard(url);
// metrics.copiedLink({ location });
});
emitter.on('closeModal', () => {

View File

@@ -134,7 +134,7 @@ details[open] > summary > svg {
transform: rotate(90deg);
}
footer li:hover {
footer li a:hover {
text-decoration: underline;
}

View File

@@ -1,4 +1,4 @@
/* global DEFAULTS LIMITS PREFS */
/* global DEFAULTS LIMITS WEB_UI PREFS */
import 'core-js';
import 'fast-text-encoding'; // MS Edge support
import 'intl-pluralrules';
@@ -10,7 +10,6 @@ import controller from './controller';
import dragManager from './dragManager';
import pasteManager from './pasteManager';
import storage from './storage';
import metrics from './metrics';
import experiments from './experiments';
import * as Sentry from '@sentry/browser';
import './main.css';
@@ -51,6 +50,7 @@ if (process.env.NODE_ENV === 'production') {
window.initialState = {
LIMITS,
DEFAULTS,
WEB_UI,
PREFS,
archive: new Archive([], DEFAULTS.EXPIRE_SECONDS),
capabilities,
@@ -67,7 +67,6 @@ if (process.env.NODE_ENV === 'production') {
// eslint-disable-next-line require-atomic-updates
window.app = app;
app.use(experiments);
app.use(metrics);
app.use(controller);
app.use(dragManager);
app.use(pasteManager);

View File

@@ -1,186 +0,0 @@
import storage from './storage';
import { platform, locale } from './utils';
import { sendMetrics } from './api';
let appState = null;
let experiment = null;
const HOUR = 1000 * 60 * 60;
const events = [];
let session_id = Date.now();
const lang = locale();
export default function initialize(state, emitter) {
appState = state;
emitter.on('DOMContentLoaded', () => {
experiment = storage.enrolled;
if (!appState.user.firstAction) {
appState.user.firstAction =
appState.route === '/' ? 'upload' : 'download';
}
const query = appState.query;
addEvent('client_visit', {
entrypoint: appState.route === '/' ? 'upload' : 'download',
referrer: document.referrer,
utm_campaign: query.utm_campaign,
utm_content: query.utm_content,
utm_medium: query.utm_medium,
utm_source: query.utm_source,
utm_term: query.utm_term
});
});
emitter.on('experiment', experimentEvent);
window.addEventListener('unload', submitEvents);
}
function sizeOrder(n) {
return Math.floor(Math.log10(n));
}
function submitEvents() {
if (navigator.doNotTrack === '1') {
return;
}
sendMetrics(
new Blob(
[
JSON.stringify({
now: Date.now(),
session_id,
lang,
platform: platform(),
events
})
],
{ type: 'text/plain' } // see http://crbug.com/490015
)
);
events.splice(0);
}
async function addEvent(event_type, event_properties) {
const user_id = await appState.user.metricId();
const device_id = await appState.user.deviceId();
const ab_id = Object.keys(experiment)[0];
if (ab_id) {
event_properties.experiment = ab_id;
event_properties.variant = experiment[ab_id];
}
events.push({
device_id,
event_properties,
event_type,
time: Date.now(),
user_id,
user_properties: {
anonymous: !appState.user.loggedIn,
first_action: appState.user.firstAction,
active_count: storage.files.length
}
});
if (events.length === 25) {
submitEvents();
}
}
function cancelledUpload(archive, duration) {
return addEvent('client_upload', {
download_limit: archive.dlimit,
duration: sizeOrder(duration),
file_count: archive.numFiles,
password_protected: !!archive.password,
size: sizeOrder(archive.size),
status: 'cancel',
time_limit: archive.timeLimit
});
}
function completedUpload(archive, duration) {
return addEvent('client_upload', {
download_limit: archive.dlimit,
duration: sizeOrder(duration),
file_count: archive.numFiles,
password_protected: !!archive.password,
size: sizeOrder(archive.size),
status: 'ok',
time_limit: archive.timeLimit
});
}
function stoppedUpload(archive, duration = 0) {
return addEvent('client_upload', {
download_limit: archive.dlimit,
duration: sizeOrder(duration),
file_count: archive.numFiles,
password_protected: !!archive.password,
size: sizeOrder(archive.size),
status: 'error',
time_limit: archive.timeLimit
});
}
function stoppedDownload(params) {
return addEvent('client_download', {
duration: sizeOrder(params.duration),
password_protected: params.password_protected,
size: sizeOrder(params.size),
status: 'error'
});
}
function completedDownload(params) {
return addEvent('client_download', {
duration: sizeOrder(params.duration),
password_protected: params.password_protected,
size: sizeOrder(params.size),
status: 'ok'
});
}
function deletedUpload(ownedFile) {
return addEvent('client_delete', {
age: Math.floor((Date.now() - ownedFile.createdAt) / HOUR),
downloaded: ownedFile.dtotal > 0,
status: 'ok'
});
}
function experimentEvent(params) {
return addEvent('client_experiment', params);
}
function submittedSignup(params) {
return addEvent('client_login', {
status: 'ok',
trigger: params.trigger
});
}
function canceledSignup(params) {
return addEvent('client_login', {
status: 'cancel',
trigger: params.trigger
});
}
function loggedOut(params) {
addEvent('client_logout', {
status: 'ok',
trigger: params.trigger
});
// flush events and start new anon session
submitEvents();
session_id = Date.now();
}
export {
cancelledUpload,
stoppedUpload,
completedUpload,
deletedUpload,
stoppedDownload,
completedDownload,
submittedSignup,
canceledSignup,
loggedOut
};

View File

@@ -580,7 +580,7 @@ module.exports.preview = function(state, emit) {
function download(event) {
event.preventDefault();
event.target.disabled = true;
emit('download', archive);
emit('download');
}
};

View File

@@ -13,19 +13,71 @@ class Footer extends Component {
createElement() {
const translate = this.state.translate;
// Add additional links from configuration if available
var links = [];
if (this.state != undefined && this.state.WEB_UI != undefined) {
const WEB_UI = this.state.WEB_UI;
if (WEB_UI.FOOTER_DONATE_URL != '') {
links.push(html`
<li class="m-2">
<a href="${WEB_UI.FOOTER_DONATE_URL}" target="_blank">
${translate('footerLinkDonate')}
</a>
</li>
`);
}
if (WEB_UI.FOOTER_CLI_URL != '') {
links.push(html`
<li class="m-2">
<a href="${WEB_UI.FOOTER_CLI_URL}" target="_blank">
${translate('footerLinkCli')}
</a>
</li>
`);
}
if (WEB_UI.FOOTER_DMCA_URL != '') {
links.push(html`
<li class="m-2">
<a href="${WEB_UI.FOOTER_DMCA_URL}" target="_blank">
${translate('footerLinkDmca')}
</a>
</li>
`);
}
if (WEB_UI.FOOTER_SOURCE_URL != '') {
links.push(html`
<li class="m-2">
<a href="${WEB_UI.FOOTER_SOURCE_URL}" target="_blank">
${translate('footerLinkSource')}
</a>
</li>
`);
}
} else {
links.push(html`
<li class="m-2">
<a href="https://gitlab.com/timvisee/send" target="_blank">
${translate('footerLinkSource')}
</a>
</li>
`);
}
return html`
<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"
>
<div>${translate('footerText')}</div>
<ul
class="flex flex-col md:flex-row items-start md:items-center md:justify-start"
>
<li class="m-2">${translate('footerText')}</li>
</ul>
<ul
class="flex flex-col md:flex-row items-start md:items-center md:justify-end"
>
<li class="m-2">
<a href="https://gitlab.com/timvisee/send"
>${translate('footerLinkSource')}</a
>
</li>
${links}
</ul>
</footer>
`;

View File

@@ -16,6 +16,7 @@ class Header extends Component {
this.account.render();
return false;
}
createElement() {
const title =
platform() === 'android'

View File

@@ -100,7 +100,7 @@ module.exports = function(state, emit) {
);
break;
case 'download':
emit('download', archive);
emit('download');
break;
}
return false;

View File

@@ -1,9 +1,8 @@
const html = require('choo/html');
const assets = require('../../common/assets');
const { bytes } = require('../utils');
const { canceledSignup, submittedSignup } = require('../metrics');
module.exports = function(trigger) {
module.exports = function() {
return function(state, emit, close) {
const DAYS = Math.floor(state.LIMITS.MAX_EXPIRE_SECONDS / 86400);
let submitting = false;
@@ -72,7 +71,6 @@ module.exports = function(trigger) {
}
function cancel(event) {
canceledSignup({ trigger });
close(event);
}
@@ -85,7 +83,6 @@ module.exports = function(trigger) {
const el = document.getElementById('email-input');
const email = el.value;
submittedSignup({ trigger });
emit('login', emailish(email) ? email : null);
}
};

View File

@@ -109,27 +109,8 @@ export default class User {
async startAuthFlow(trigger, utms = {}) {
this.utms = utms;
this.trigger = trigger;
try {
const params = new URLSearchParams({
entrypoint: `send-${trigger}`,
form_type: 'email',
utm_source: utms.source || 'send',
utm_campaign: utms.campaign || 'none'
});
const res = await fetch(
`${this.authConfig.issuer}/metrics-flow?${params.toString()}`,
{
mode: 'cors'
}
);
const { flowId, flowBeginTime } = await res.json();
this.flowId = flowId;
this.flowBeginTime = flowBeginTime;
} catch (e) {
console.error(e);
this.flowId = null;
this.flowBeginTime = null;
}
this.flowId = null;
this.flowBeginTime = null;
}
async login(email) {

View File

@@ -40,6 +40,7 @@ Of course, we don't want to expose the service on port 1443. Instead we want our
* a2enmod proxy
* a2enmod proxy_http
* a2enmod proxy_wstunnel
* a2enmod rewrite
In your Apache virtual host configuration file, insert this:

View File

@@ -12,18 +12,24 @@ Or run `docker build -t send:latest .` to create an image locally or `docker-com
| Name | Description
|------------------|-------------|
| `BASE_URL` | The HTTPS URL where traffic will be served (e.g. `https://send.firefox.com`)
| `PORT` | Port the server will listen on (defaults to 1443).
| `NODE_ENV` | `"production"`
| `FILE_DIR` | Uploads directory for local storage
| `S3_BUCKET` | The S3 bucket name.
| `S3_ENDPOINT`| Optional custom S3 endpoint host.
| `S3_USE_PATH_STYLE_ENDPOINTS`| `true` or `false`
| `AWS_ACCESS_KEY_ID` | S3 access key ID
| `AWS_SECRET_ACCESS_KEY` | S3 secret access key ID
| `MAX_FILE_SIZE` | Maximum upload file size in bytes (defaults to 2147483648)
| `MAX_EXPIRE_SECONDS` | Maximum upload expiry time in seconds (defaults to 604800)
| `REDIS_HOST` | Host name of the Redis server.
| `SENTRY_CLIENT` | Sentry Client ID
| `SENTRY_DSN` | Sentry DSN
| `MAX_FILE_SIZE` | in bytes (defaults to 2147483648)
| `NODE_ENV` | "production"
| `BASE_URL` | The HTTPS URL where traffic will be served (e.g. `https://send.firefox.com`)
## Example:
```sh
```bash
$ docker run --net=host -e 'NODE_ENV=production' \
-e 'S3_BUCKET=testpilot-p2p-dev' \
-e 'REDIS_HOST=dyf9s2r4vo3.bolxr4.0001.usw2.cache.amazonaws.com' \
@@ -32,3 +38,9 @@ $ docker run --net=host -e 'NODE_ENV=production' \
-e 'BASE_URL=https://send.firefox.com' \
registry.gitlab.com/timvisee/send:latest
```
## Docker compose
For a Docker compose configuration example, see:
https://github.com/timvisee/send-docker-compose

View File

@@ -1,128 +0,0 @@
# Send V2 Metrics Definitions
## Key Value Prop
Quickly and privately transfer large files from any device to any device.
## Key Business Question to Answer
Is the value proposition of a large encrypted file transfer service enough to drive Firefox Account relationships for non-Firefox users.
## Hypotheses to Test
### Primary - In support of Relationships KPI
We believe that a privacy-respecting file transfer service can drive Firefox Accounts beyond the Firefox Browser.
We will know this to be true when we see 250k Firefox Account creations from non-Firefox contexts w/in six months of launch.
### Secondary - In support of Revenue KPI
We believe that a privacy respecting service accessible beyond the reach of Firefox will provide a valuable platform to research, communicate with, and market to conscious choosers we have traditionally found hard to reach.
We will know this to be true when we can conduct six research tasks (surveys, A/B tests, fake doors, etc) in support of premium services KPIs in the first six months after launch.
## Overview of Key Measures
* Number of people using the service to send and receive files
* Why: measure of service size. Important for understanding addressable market size
* Percent of users who have or create an FxAccount via Send
* Why: representation of % of any service users who might be amenable to an upsell
* % of downloaders who convert into uploaders
* Why: represents a measure of our key growth-loop potential
* Count of uploads and size
* Why: Represents cost of service on a running basis
## Key Funnels
* App Open or Visit `--- DESIRED OUTCOME --->` Successful Upload
* Download UI Visit `--- DESIRED OUTCOME --->` Successful Download
* FxA UI Engagement `--- DESIRED OUTCOME --->` Authenticate
* **STRETCH** App Open or Visit `--- DESIRED OUTCOME --->` Successful Download
## Amplitude Schema
Please see, **See Amplitude HTTP API**(https://amplitude.zendesk.com/hc/en-us/articles/204771828) for HTTP API reference.
## Metric Events
In support of our KPIs we collect events from two separate contexts, server and client. The events are designed to have minimal correlation between contexts.
Server events collect lifecycle information about individual uploads but no user information; also time precision is truncated to hour increments. Client events collect information about how users interact with the UI but no upload identifiers.
### Server Events
Server events allow us to aggregate data about file lifecycle without collecting data about individual users. In this context `user_id` and `user_properties` describe the uploaded archive.
* `session_id` -1 (not part of a session)
* `user_id` hash of (archive_id + owner_id)
* `app_version` package.json version
* `time` timestamp truncated to hour precision
* `country`
* `region`
* `event_type` [server_upload | server_download | server_delete]
* `user_properties`
* `download_limit` set number of downloads
* `time_limit` set expiry duration
* `size` approximate size (log10)
* `anonymous` true if anonymous, false if fxa
* `event_properties`
* `download_count` downloads completed
* `ttl` time remaining before expiry truncated to hour
* `agent` the browser name or first 6 characters of the user agent that made the request
### Client Events
Client events allow us to aggregate data about how the user interface is being used without tracking the lifecycle of individual files. In this context `user_id` and `user_properties` describe the user. The `user_id` and `device_id` change for all users at the beginning of each month.
* `session_id` timestamp
* `user_id` hash of (fxa_id + Date.year + Date.month)
* `device_id` hash of (localStorage random id + Date.year + Date.month)
* `platform` [web | android]
* `country`
* `region`
* `language`
* `time` timestamp
* `os_name`
* `event_type` [client_visit | client_upload | client_download | client_delete | client_login | client_logout]
* `event_properties`
* `browser`
* `browser_version`
* `status` [ ok | error | cancel ]
* Event specific properties (see below)
* `user_properties`
* `active_count` number of active uploads
* `anonymous` true if anonymous, false if fxa
* `experiments` list of experiment ids the user is participating in
* `first_action` how this use came to Send the first time [ upload | download ]
#### Visit Event
* `entrypoint` [ upload | download ]
#### Upload Event
* `download_limit` download limit
* `file_count` number of files
* `password_protected` boolean
* `size` approximate size (log10)
* `time_limit` time limit
* `duration` approximate transfer duration (log10)
#### Download Event
* `password_protected` boolean
* `size` approximate size (log10)
* `duration` approximate transfer duration (log10)
#### Delete Event
* `age` hours since uploaded
* `downloaded` downloaded at least once
#### Login Event
* `trigger` [button | time | count | size]
#### Logout Event
* `trigger` [button | timeout]

2862
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
{
"name": "send",
"description": "File Sharing Experiment",
"version": "3.4.0",
"version": "3.4.5",
"author": "Mozilla (https://mozilla.org)",
"contributors": [
"Tim Visee <3a4fb3964f@sinenomine.email> (https://timvisee.com)"
@@ -61,24 +61,24 @@
"cache": true
},
"engines": {
"node": "^12.16.3"
"node": "^15.5.1"
},
"devDependencies": {
"@babel/core": "^7.12.10",
"@babel/plugin-proposal-class-properties": "^7.12.1",
"@babel/core": "^7.13.10",
"@babel/plugin-proposal-class-properties": "^7.13.0",
"@babel/plugin-syntax-dynamic-import": "^7.2.0",
"@babel/preset-env": "^7.12.11",
"@babel/preset-env": "^7.13.10",
"@dannycoates/webcrypto-liner": "^0.1.37",
"@fullhuman/postcss-purgecss": "^1.3.0",
"@mattiasbuelens/web-streams-polyfill": "0.2.1",
"@sentry/browser": "^5.29.2",
"@sentry/browser": "^5.30.0",
"asmcrypto.js": "^0.22.0",
"babel-loader": "^8.2.2",
"babel-plugin-istanbul": "^5.2.0",
"base64-js": "^1.5.1",
"content-disposition": "^0.5.3",
"copy-webpack-plugin": "^5.1.2",
"core-js": "^3.8.2",
"core-js": "^3.9.1",
"crc": "^3.8.0",
"cross-env": "^6.0.3",
"css-loader": "^3.6.0",
@@ -102,7 +102,7 @@
"lint-staged": "^9.4.2",
"mocha": "^6.2.2",
"morgan": "^1.9.1",
"nanobus": "^4.4.0",
"nanobus": "^4.5.0",
"nanohtml": "^1.9.0",
"nanotiming": "^7.3.1",
"npm-run-all": "^4.1.5",
@@ -113,22 +113,21 @@
"proxyquire": "^2.1.3",
"puppeteer": "^2.0.0",
"raw-loader": "^3.1.0",
"redis-mock": "^0.47.0",
"rimraf": "^3.0.0",
"script-loader": "^0.7.2",
"sinon": "^7.5.0",
"string-hash": "^1.1.3",
"stylelint": "^13.8.0",
"stylelint": "^13.12.0",
"stylelint-config-standard": "^19.0.0",
"stylelint-no-unsupported-browser-features": "^4.1.4",
"svgo": "^1.3.2",
"svgo-loader": "^2.2.1",
"svgo-loader": "^2.2.2",
"tailwindcss": "^1.9.6",
"val-loader": "^1.1.1",
"webpack": "4.38.0",
"webpack-cli": "^3.3.12",
"webpack-dev-middleware": "^3.7.3",
"webpack-dev-server": "^3.11.1",
"webpack-dev-server": "^3.11.2",
"webpack-manifest-plugin": "^2.2.0",
"webpack-unassert-loader": "^1.2.0"
},
@@ -136,9 +135,9 @@
"@dannycoates/express-ws": "^5.0.3",
"@fluent/bundle": "^0.13.0",
"@fluent/langneg": "^0.3.0",
"@google-cloud/storage": "^5.7.1",
"@sentry/node": "^5.29.2",
"aws-sdk": "^2.824.0",
"@google-cloud/storage": "^5.8.1",
"@sentry/node": "^5.30.0",
"aws-sdk": "^2.864.0",
"body-parser": "^1.19.0",
"choo": "^7.0.0",
"cldr-core": "^35.1.0",
@@ -150,8 +149,9 @@
"mozlog": "^2.2.0",
"node-fetch": "^2.6.1",
"redis": "^2.8.0",
"redis-mock": "^0.47.0",
"selenium-standalone": "^6.23.0",
"ua-parser-js": "^0.7.23"
"ua-parser-js": "^0.7.24"
},
"availableLanguages": [
"en-US",

View File

@@ -29,6 +29,9 @@ updateFirefox = Update Firefox
deletePopupCancel = Cancel
deleteButtonHover = Delete
footerText = Not affiliated with Mozilla or Firefox.
footerLinkDonate = Donate
footerLinkCli = CLI
footerLinkDmca = DMCA
footerLinkSource = Source
passwordTryAgain = Incorrect password. Try again.
javascriptRequired = Send requires JavaScript

View File

@@ -27,6 +27,9 @@ updateFirefox = Update Firefox
deletePopupCancel = Cancel
deleteButtonHover = Delete
footerText = Not affiliated with Mozilla or Firefox.
footerLinkDonate = Donate
footerLinkCli = CLI
footerLinkDmca = DMCA
footerLinkSource = Source
passwordTryAgain = Incorrect password. Try again.
javascriptRequired = Send requires JavaScript

View File

@@ -29,6 +29,9 @@ updateFirefox = Firefox bijwerken
deletePopupCancel = Annuleren
deleteButtonHover = Verwijderen
footerText = Niet aangesloten aan Mozilla of Firefox.
footerLinkDonate = Doneren
footerLinkCli = CLI
footerLinkDmca = DMCA
footerLinkSource = Broncode
passwordTryAgain = Onjuist wachtwoord. Probeer het opnieuw.
javascriptRequired = Send vereist JavaScript

39
s3-lifecycle-example.xml Normal file
View File

@@ -0,0 +1,39 @@
<?xml version="1.0" ?>
<LifecycleConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
<!-- remove files for 1 day after 1 day -->
<Rule>
<ID>1skjet8gxudyans73v3p3d6hvq6yn3w02kq7931h81ohopd3</ID>
<Prefix>1-</Prefix>
<Status>Enabled</Status>
<Expiration>
<Days>1</Days>
</Expiration>
</Rule>
<!-- remove files for 7 days after 7 days -->
<Rule>
<ID>1skjet8gxudyans73v3p3d6hvq6yn3w02kq7931h81ohopd4</ID>
<Prefix>7-</Prefix>
<Status>Enabled</Status>
<Expiration>
<Days>7</Days>
</Expiration>
</Rule>
<!-- remove all files after 1 month -->
<Rule>
<ID>1skjet8gxudyans73v3p3d6hvq6yn3w02kq7931h81ohopd5</ID>
<Prefix/>
<Status>Enabled</Status>
<Expiration>
<Days>30</Days>
</Expiration>
</Rule>
<!-- remove aborted uploads after 1 day -->
<Rule>
<ID>1skjet8gxudyans73v3p3d6hvq6yn3w02kq7931h81ohopd6</ID>
<Prefix></Prefix>
<Status>Enabled</Status>
<AbortIncompleteMultipartUpload>
<DaysAfterInitiation>1</DaysAfterInitiation>
</AbortIncompleteMultipartUpload>
</Rule>
</LifecycleConfiguration>

View File

@@ -1,171 +0,0 @@
const crypto = require('crypto');
const fetch = require('node-fetch');
const config = require('./config');
const pkg = require('../package.json');
const HOUR = 1000 * 60 * 60;
function truncateToHour(timestamp) {
return Math.floor(timestamp / HOUR) * HOUR;
}
function orderOfMagnitude(n) {
return Math.floor(Math.log10(n));
}
function userId(fileId, ownerId) {
const hash = crypto.createHash('sha256');
hash.update(fileId);
hash.update(ownerId);
return hash.digest('hex').substring(32);
}
function statUploadEvent(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_upload',
user_properties: {
download_limit: data.dlimit,
time_limit: data.timeLimit,
size: orderOfMagnitude(data.size),
anonymous: data.anonymous
},
event_properties: {
agent: data.agent
},
event_id: 0
};
return sendBatch([event]);
}
function statDownloadEvent(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_download',
event_properties: {
agent: data.agent,
download_count: data.download_count,
ttl: data.ttl
},
event_id: data.download_count
};
return sendBatch([event]);
}
function statDeleteEvent(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_delete',
event_properties: {
agent: data.agent,
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 = {
browser: ua.browser.name,
browser_version: ua.browser.version,
status: ep.status,
age: ep.age,
downloaded: ep.downloaded,
download_limit: ep.download_limit,
duration: ep.duration,
entrypoint: ep.entrypoint,
file_count: ep.file_count,
password_protected: ep.password_protected,
referrer: ep.referrer,
size: ep.size,
time_limit: ep.time_limit,
trigger: ep.trigger,
ttl: ep.ttl,
utm_campaign: ep.utm_campaign,
utm_content: ep.utm_content,
utm_medium: ep.utm_medium,
utm_source: ep.utm_source,
utm_term: ep.utm_term,
experiment: ep.experiment,
variant: ep.variant
};
const user_properties = {
active_count: up.active_count,
anonymous: up.anonymous,
experiments: up.experiments,
first_action: up.first_action
};
return {
app_version: pkg.version,
country: country,
device_id: event.device_id,
event_properties,
event_type: event.event_type,
language,
os_name: ua.os.name,
os_version: ua.os.version,
platform,
region: state,
session_id,
time: event.time + deltaT,
user_id: event.user_id,
user_properties
};
}
async function sendBatch(events, timeout = 1000) {
if (!config.amplitude_id) {
return 200;
}
try {
const result = await fetch('https://api.amplitude.com/batch', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
api_key: config.amplitude_id,
events
}),
timeout
});
return result.status;
} catch (e) {
return 500;
}
}
module.exports = {
statUploadEvent,
statDownloadEvent,
statDeleteEvent,
clientEvent,
sendBatch
};

View File

@@ -13,6 +13,12 @@ module.exports = {
MAX_FILES_PER_ARCHIVE: config.max_files_per_archive,
MAX_ARCHIVES_PER_USER: config.max_archives_per_user
},
WEB_UI: {
FOOTER_DONATE_URL: config.footer_donate_url,
FOOTER_CLI_URL: config.footer_cli_url,
FOOTER_DMCA_URL: config.footer_dmca_url,
FOOTER_SOURCE_URL: config.footer_source_url
},
DEFAULTS: {
DOWNLOAD_COUNTS: config.download_counts,
EXPIRE_TIMES_SECONDS: config.expire_times_seconds,

View File

@@ -100,16 +100,6 @@ const conf = convict({
arg: 'port',
env: 'PORT'
},
amplitude_id: {
format: String,
default: '',
env: 'AMPLITUDE_ID'
},
analytics_id: {
format: String,
default: '',
env: 'GOOGLE_ANALYTICS_ID'
},
sentry_id: {
format: String,
default: '',
@@ -194,6 +184,26 @@ const conf = convict({
format: String,
default: '',
env: 'IP_DB'
},
footer_donate_url: {
format: String,
default: '',
env: 'SEND_FOOTER_DONATE_URL'
},
footer_cli_url: {
format: String,
default: 'https://github.com/timvisee/ffsend',
env: 'SEND_FOOTER_CLI_URL'
},
footer_dmca_url: {
format: String,
default: '',
env: 'SEND_FOOTER_DMCA_URL'
},
footer_source_url: {
format: String,
default: 'https://github.com/timvisee/send',
env: 'SEND_FOOTER_SOURCE_URL'
}
});

View File

@@ -43,6 +43,7 @@ module.exports = function(state) {
}
var LIMITS = ${JSON.stringify(clientConstants.LIMITS)};
var WEB_UI = ${JSON.stringify(clientConstants.WEB_UI)};
var DEFAULTS = ${JSON.stringify(clientConstants.DEFAULTS)};
var PREFS = ${JSON.stringify(state.prefs)};
var downloadMetadata = ${

View File

@@ -1,23 +1,10 @@
const storage = require('../storage');
const { statDeleteEvent } = require('../amplitude');
module.exports = async function(req, res) {
try {
const id = req.params.id;
const meta = req.meta;
const ttl = await storage.ttl(id);
await storage.del(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,
agent: req.ua.browser.name || req.ua.ua.substring(0, 6)
});
} catch (e) {
res.sendStatus(404);
}

View File

@@ -1,7 +1,6 @@
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;
@@ -27,17 +26,6 @@ module.exports = async function(req, res) {
const dl = meta.dl + 1;
const dlimit = meta.dlimit;
const ttl = await storage.ttl(id);
statDownloadEvent({
id,
ip: req.ip,
country: req.geo.country,
state: req.geo.state,
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);

View File

@@ -112,7 +112,6 @@ module.exports = function(app) {
require('./params')
);
app.post(`/api/info/:id${ID_REGEX}`, auth.owner, require('./info'));
app.post('/api/metrics', require('./metrics'));
app.get('/__version__', function(req, res) {
// eslint-disable-next-line node/no-missing-require
res.sendFile(require.resolve('../../dist/version.json'));

View File

@@ -1,24 +0,0 @@
const { sendBatch, clientEvent } = require('../amplitude');
module.exports = async function(req, res) {
try {
const data = JSON.parse(req.body); // see http://crbug.com/490015
const deltaT = Date.now() - data.now;
const events = data.events.map(e =>
clientEvent(
e,
req.ua,
data.lang,
data.session_id + deltaT,
deltaT,
data.platform,
req.geo.country,
req.geo.state
)
);
const status = await sendBatch(events);
res.sendStatus(status);
} catch (e) {
res.sendStatus(500);
}
};

View File

@@ -4,7 +4,6 @@ const config = require('../config');
const mozlog = require('../log');
const Limiter = require('../limiter');
const fxa = require('../fxa');
const { statUploadEvent } = require('../amplitude');
const { encryptedSize } = require('../../app/utils');
const { Transform } = require('stream');
@@ -108,18 +107,6 @@ module.exports = function(ws, req) {
// in order to avoid having to check socket state and clean
// up storage, possibly with an exception that we can catch.
ws.send(JSON.stringify({ ok: true }));
statUploadEvent({
id: newId,
ip: req.ip,
country: req.geo.country,
state: req.geo.state,
owner,
dlimit,
timeLimit,
anonymous: !user,
size: limiter.length,
agent: req.ua.browser.name || req.ua.ua.substring(0, 6)
});
}
} catch (e) {
log.error('upload', e);

View File

@@ -6,9 +6,9 @@ class S3Storage {
this.log = log;
const cfg = {};
if (config.s3_endpoint != '') {
cfg['endpoint'] = config.s3_endpoint;
cfg['endpoint'] = config.s3_endpoint;
}
cfg['s3ForcePathStyle'] = config.s3_use_path_style_endpoint
cfg['s3ForcePathStyle'] = config.s3_use_path_style_endpoint;
AWS.config.update(cfg);
this.s3 = new AWS.S3();
}
@@ -21,7 +21,9 @@ class S3Storage {
}
getStream(id) {
return this.s3.getObject({ Bucket: this.bucket, Key: id }).createReadStream();
return this.s3
.getObject({ Bucket: this.bucket, Key: id })
.createReadStream();
}
set(id, file) {