From e9339dc378871bb932c29f264c25a1436cf1f959 Mon Sep 17 00:00:00 2001
From: Jeremie Pardou-Piquemal <571533+jrmi@users.noreply.github.com>
Date: Sat, 2 May 2020 10:08:00 +0200
Subject: [PATCH] Fix and enhance FileTransfer component
---
client/.env.dist | 11 +-
.../components/FileTransfer/FileTransfer.js | 123 +++++++++++++
.../FileTransfer/FileTransfer.test.js | 172 ++++++++++--------
.../__snapshots__/FileTransfer.test.js.snap | 2 +-
client/src/components/FileTransfer/index.js | 117 +-----------
client/src/utils/index.js | 2 +-
readme.md | 4 +-
7 files changed, 233 insertions(+), 198 deletions(-)
create mode 100644 client/src/components/FileTransfer/FileTransfer.js
diff --git a/client/.env.dist b/client/.env.dist
index 07e63f9..f577e32 100644
--- a/client/.env.dist
+++ b/client/.env.dist
@@ -1,5 +1,14 @@
+# Api settings
+TZ=UTC
REACT_APP_API_HOST=localhost
REACT_APP_API_PROTOCOL=http
REACT_APP_API_PORT=3001
REACT_APP_COMMIT_SHA=some_sha
-TZ=UTC
+
+# To display darkwire version
+REACT_APP_COMMIT_SHA=some_sha
+
+# Set max transferable file size in MB
+REACT_APP_MAX_FILE_SIZE=4
+
+
diff --git a/client/src/components/FileTransfer/FileTransfer.js b/client/src/components/FileTransfer/FileTransfer.js
new file mode 100644
index 0000000..fb093a4
--- /dev/null
+++ b/client/src/components/FileTransfer/FileTransfer.js
@@ -0,0 +1,123 @@
+import React from 'react'
+import PropTypes from 'prop-types'
+import uuid from 'uuid'
+import { File } from 'react-feather'
+import { sanitize } from 'utils'
+import { styles } from './styles.module.scss'
+
+const VALID_FILE_TYPES = ['png', 'jpg', 'jpeg', 'gif', 'zip', 'rar', 'gzip', 'pdf', 'txt', 'json', 'doc', 'docx', 'csv', 'js', 'html', 'css']
+
+const MAX_FILE_SIZE = process.env.REACT_APP_MAX_FILE_SIZE || 4
+
+/**
+ * Encode the given file to binary string
+ * @param {File} file
+ */
+const encodeFile = (file) => {
+ return new Promise((resolve, reject) => {
+ const reader = new window.FileReader()
+
+ if (!file) {
+ reject()
+ return
+ }
+
+ reader.onload = (readerEvent) => {
+ resolve(window.btoa(readerEvent.target.result))
+ }
+
+ reader.readAsBinaryString(file)
+ })
+}
+
+export const FileTransfer = ({ sendEncryptedMessage }) => {
+ const fileInput = React.useRef(null);
+
+ const supported = React.useMemo(() =>
+ Boolean(window.File) && Boolean(window.FileReader) && Boolean(window.FileList) && Boolean(window.Blob) &&
+ Boolean(window.btoa) && Boolean(window.atob) && Boolean(window.URL),
+ []
+ )
+
+ React.useEffect(() => {
+ const currentFileInput = fileInput.current
+ let isMounted = true;
+
+ const handleFileTransfer = async (event) => {
+
+ const file = event.target.files && event.target.files[0]
+
+ if (file) {
+ const fileType = file.type || 'file'
+ const fileName = sanitize(file.name)
+ const fileExtension = file.name.split('.').pop().toLowerCase()
+
+ if (VALID_FILE_TYPES.indexOf(fileExtension) <= -1) {
+ // eslint-disable-next-line no-alert
+ alert('File type not supported')
+ return false
+ }
+
+ if (file.size > MAX_FILE_SIZE * 1000000) {
+ // eslint-disable-next-line no-alert
+ alert(`Max filesize is ${MAX_FILE_SIZE}MB`)
+ return false
+ }
+
+ const fileId = uuid.v4()
+ const fileData = {
+ id: fileId,
+ file,
+ fileName,
+ fileType,
+ encodedFile: await encodeFile(file),
+ }
+
+ // Mounted component guard
+ if (!isMounted) {
+ return
+ }
+
+ fileInput.current.value = ''
+
+ sendEncryptedMessage({
+ type: 'SEND_FILE',
+ payload: {
+ fileName: fileData.fileName,
+ encodedFile: fileData.encodedFile,
+ fileType: fileData.fileType,
+ timestamp: Date.now(),
+ },
+ })
+ }
+
+ return false
+ }
+
+ currentFileInput.addEventListener('change', handleFileTransfer)
+
+ return () => {
+ isMounted = false
+ currentFileInput.removeEventListener('change', handleFileTransfer)
+ }
+ }, [sendEncryptedMessage])
+
+ if (!supported) {
+ return null
+ }
+
+ return (
+
+
+
+
+
+
+ );
+}
+
+FileTransfer.propTypes = {
+ sendEncryptedMessage: PropTypes.func.isRequired,
+}
+
+export default FileTransfer
\ No newline at end of file
diff --git a/client/src/components/FileTransfer/FileTransfer.test.js b/client/src/components/FileTransfer/FileTransfer.test.js
index 73ca8d3..052a9bc 100644
--- a/client/src/components/FileTransfer/FileTransfer.test.js
+++ b/client/src/components/FileTransfer/FileTransfer.test.js
@@ -2,89 +2,103 @@ import React from 'react';
import { render, screen, fireEvent, createEvent } from '@testing-library/react';
import FileTransfer from '.';
-test('FileTransfer component is displaying', async () => {
- const { asFragment } = render( {}} />);
+// Fake date
+jest.spyOn(global.Date, 'now').mockImplementation(() => new Date('2020-03-14T11:01:58.135Z').valueOf());
- expect(asFragment()).toMatchSnapshot();
-});
-
-// Skipped as broken in this component version. Should be fixed later.
-test.skip('FileTransfer component detect bad browser support', async () => {
+describe('FileTransfer tests', () => {
const { File } = window;
- // Remove one of component dependency
- delete window.File;
+ beforeEach(() => {
+ // Restore original
+ window.File = File;
+ });
+ afterEach(() => {
+ // Restore original
+ window.File = File;
+ });
- const { asFragment } = render( {}} />);
+ it('FileTransfer component is displaying', async () => {
+ const { asFragment } = render( {}} />);
- expect(asFragment()).toMatchSnapshot();
+ expect(asFragment()).toMatchSnapshot();
+ });
- window.File = File;
-});
-
-test('Try to send file', async done => {
- const sendEncryptedMessage = data => {
- try {
- expect(data).toMatchObject({
- payload: { encodedFile: 'dGV4dGZpbGU=', fileName: 'filename-png', fileType: 'text/plain' },
- type: 'SEND_FILE',
- });
- done();
- } catch (error) {
- done(error);
- }
- };
-
- render( );
-
- const input = screen.getByPlaceholderText('Choose a file...');
-
- const testFile = new File(['textfile'], 'filename.png', { type: 'text/plain' });
-
- // Fire change event
- fireEvent.change(input, { target: { files: [testFile] } });
-});
-
-test('Try to send no file', async () => {
- render( {}} />);
-
- const input = screen.getByPlaceholderText('Choose a file...');
-
- // Fire change event
- fireEvent.change(input, { target: { files: [] } });
-});
-
-test('Try to send unsupported file', async () => {
- window.alert = jest.fn();
-
- render( {}} />);
-
- const input = screen.getByPlaceholderText('Choose a file...');
-
- const testFile = new File(['textfile'], 'filename.fake', { type: 'text/plain' });
-
- // Create thange event with fake file
- const changeEvent = createEvent.change(input, { target: { files: [testFile] } });
-
- // Fire change event
- fireEvent(input, changeEvent);
-
- expect(window.alert).toHaveBeenCalledWith('File type not supported');
-});
-
-test('Try to send too big file', async () => {
- window.alert = jest.fn();
-
- render( {}} />);
-
- const input = screen.getByPlaceholderText('Choose a file...');
-
- var fileContent = new Uint8Array(4000001);
-
- const testFile = new File([fileContent], 'filename.png', { type: 'text/plain' });
-
- // Fire change event
- fireEvent.change(input, { target: { files: [testFile] } });
-
- expect(window.alert).toHaveBeenCalledWith('Max filesize is 4MB');
+ // Skipped as broken in this component version. Should be fixed later.
+ it.skip('FileTransfer component detect bad browser support', () => {
+ const { asFragment } = render( {}} />);
+
+ expect(asFragment()).toMatchSnapshot();
+ });
+
+ it('Try to send file', async done => {
+ const sendEncryptedMessage = data => {
+ try {
+ expect(data).toMatchObject({
+ payload: {
+ encodedFile: 'dGV4dGZpbGU=',
+ fileName: 'filename.png',
+ fileType: 'text/plain',
+ timestamp: 1584183718135,
+ },
+ type: 'SEND_FILE',
+ });
+ done();
+ } catch (error) {
+ done(error);
+ }
+ };
+
+ render( );
+
+ const input = screen.getByPlaceholderText('Choose a file...');
+
+ const testFile = new File(['textfile'], 'filename.png', { type: 'text/plain' });
+
+ // Fire change event
+ fireEvent.change(input, { target: { files: [testFile] } });
+ });
+
+ it('Try to send no file', async () => {
+ render( {}} />);
+
+ const input = screen.getByPlaceholderText('Choose a file...');
+
+ // Fire change event
+ fireEvent.change(input, { target: { files: [] } });
+ });
+
+ it('Try to send unsupported file', async () => {
+ window.alert = jest.fn();
+
+ render( {}} />);
+
+ const input = screen.getByPlaceholderText('Choose a file...');
+
+ const testFile = new File(['textfile'], 'filename.fake', { type: 'text/plain' });
+
+ // Create thange event with fake file
+ const changeEvent = createEvent.change(input, { target: { files: [testFile] } });
+
+ // Fire change event
+ fireEvent(input, changeEvent);
+
+ expect(window.alert).toHaveBeenCalledWith('File type not supported');
+ });
+
+ it('Try to send too big file', async () => {
+ window.alert = jest.fn();
+
+ render( {}} />);
+
+ const input = screen.getByPlaceholderText('Choose a file...');
+
+ var fileContent = new Uint8Array(4000001);
+
+ const testFile = new File([fileContent], 'filename.png', { type: 'text/plain' });
+
+ // Fire change event
+ fireEvent.change(input, { target: { files: [testFile] } });
+
+ expect(window.alert).toHaveBeenCalledWith('Max filesize is 4MB');
+ });
});
diff --git a/client/src/components/FileTransfer/__snapshots__/FileTransfer.test.js.snap b/client/src/components/FileTransfer/__snapshots__/FileTransfer.test.js.snap
index 221cab1..d66f81f 100644
--- a/client/src/components/FileTransfer/__snapshots__/FileTransfer.test.js.snap
+++ b/client/src/components/FileTransfer/__snapshots__/FileTransfer.test.js.snap
@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`FileTransfer component is displaying 1`] = `
+exports[`FileTransfer tests FileTransfer component is displaying 1`] = `
{
- const reader = new window.FileReader()
-
- if (!file) {
- reject()
- return
- }
-
- reader.onload = (readerEvent) => {
- resolve(window.btoa(readerEvent.target.result))
- this._fileInput.value = ''
- }
-
- reader.readAsBinaryString(file)
- })
- }
-
- async confirmFileTransfer(event) {
- const file = event.target.files && event.target.files[0]
-
- if (file) {
- const fileType = file.type || 'file'
- const fileName = sanitize(file.name)
- const { localFileQueue } = this.state
- const fileExtension = file.name.split('.').pop().toLowerCase()
-
- if (VALID_FILE_TYPES.indexOf(fileExtension) <= -1) {
- // eslint-disable-next-line no-alert
- alert('File type not supported')
- return false
- }
-
- if (file.size > 4 * 1000000) {
- // eslint-disable-next-line no-alert
- alert('Max filesize is 4MB')
- return false
- }
-
- const fileId = uuid.v4()
- const fileData = {
- id: fileId,
- file,
- fileName,
- fileType,
- encodedFile: await this.encodeFile(file),
- }
-
- localFileQueue.push(fileData)
-
- this.setState({
- localFileQueue,
- }, async () => {
- this.props.sendEncryptedMessage({
- type: 'SEND_FILE',
- payload: {
- fileName: fileData.fileName,
- encodedFile: fileData.encodedFile,
- fileType: fileData.fileType,
- timestamp: Date.now(),
- },
- })
- })
- }
-
- return false
- }
-
- render() {
- if (!this.state.supported) {
- return null
- }
- return (
-
- this._fileInput = c} />
-
-
-
-
- )
- }
-}
-
-FileTransfer.propTypes = {
- sendEncryptedMessage: PropTypes.func.isRequired,
-}
+export default FileTransfer;
diff --git a/client/src/utils/index.js b/client/src/utils/index.js
index 238abd7..2900605 100644
--- a/client/src/utils/index.js
+++ b/client/src/utils/index.js
@@ -1,4 +1,4 @@
/* eslint-disable import/prefer-default-export */
export function sanitize(str) {
- return str.replace(/[^A-Za-z0-9]/g, '-').replace(/[<>]/ig, '')
+ return str.replace(/[^A-Za-z0-9._]/g, '-').replace(/[<>]/ig, '')
}
diff --git a/readme.md b/readme.md
index 63cc5e8..3b001d4 100644
--- a/readme.md
+++ b/readme.md
@@ -87,12 +87,14 @@ Darkwire does not provide any guarantee that the person you're communicating wit
## File Transfer
-Darkwire encodes documents (up to 1MB) into base64 using [btoa](https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64/btoa) and is encrypted the same way chat messages are.
+Darkwire encodes documents into base64 using [btoa](https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64/btoa) and is encrypted the same way chat messages are.
1. When a file is "uploaded", the document is encoded on the client and the server recieves the encrypted base64 string.
2. The server sends the encrypted base64 string to clients in the same chat room.
3. Clients recieving the encrypted base64 string then decrypts and decodes the base64 string using [atob](https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64/atob).
+The default transferable file size limit is 4MB, but can be changed in `.env` file with the `REACT_APP_MAX_FILE_SIZE` variable.
+
## Sockets & Server
Darkwire uses [socket.io](http://socket.io) to transmit encrypted information using secure [WebSockets](https://en.wikipedia.org/wiki/WebSocket) (WSS).