├── .github └── workflows │ └── node.js.yml ├── .gitignore ├── Dockerfile ├── LICENSE.md ├── README.md ├── build ├── icon.icns ├── icon.ico └── icon.png ├── docker-compose.yml ├── docs ├── ActivityDiagram.drawio ├── ActivityDiagram.png ├── ClientClassDiagram.drawio ├── ClientClassDiagram.png ├── SequenceDiagramLogin.drawio ├── SequenceDiagramLogin.png ├── SequenceDiagramSendFile.drawio ├── SequenceDiagramSendFile.png ├── ServerClassDiagram.drawio ├── ServerClassDiagram.png ├── SystemArchitecture.drawio ├── SystemArchitecture.png ├── UserPersona.drawio └── UserPersona.png ├── index.html ├── jsconfig.json ├── package-lock.json ├── package.json ├── promotional-assets ├── Desktop.png ├── DockerHub.png ├── GitHub.png ├── Laptop-Application.png ├── Laptop-Login.png ├── Linux.png ├── Phone.png ├── Tablet.png ├── Windows.png └── macOS.png ├── src ├── app.js ├── assets │ ├── css │ │ ├── dark.css │ │ ├── light.css │ │ ├── resize.css │ │ └── style.css │ ├── img │ │ ├── Banner.png │ │ ├── Icon.png │ │ ├── Icon.psd │ │ └── favicon │ │ │ ├── android-chrome-192x192.png │ │ │ ├── android-chrome-512x512.png │ │ │ ├── apple-touch-icon.png │ │ │ ├── browserconfig.xml │ │ │ ├── favicon-16x16.png │ │ │ ├── favicon-32x32.png │ │ │ ├── favicon.ico │ │ │ ├── mstile-150x150.png │ │ │ ├── safari-pinned-tab.svg │ │ │ └── site.webmanifest │ └── js │ │ ├── ChunkReader.js │ │ ├── CryptoFD.js │ │ ├── FileSaver.js │ │ ├── Uploader.js │ │ ├── crypto-js.js │ │ ├── crypto-js.min.js.map │ │ ├── forge.all.js │ │ ├── forge.all.min.js.map │ │ ├── forge.js │ │ ├── jquery.js │ │ ├── main.js │ │ ├── notifier.js │ │ ├── portableMain.js │ │ ├── prime.worker.min.js │ │ ├── socket.io.min.js.map │ │ ├── socket.js │ │ └── utils.js ├── modules │ ├── Colors.js │ ├── ConnectionManager.js │ ├── DBAdapter.js │ ├── DBManager.js │ ├── PermissionManager.js │ ├── UploadManager.js │ ├── Utils.js │ └── Words.js ├── portableApp.js ├── server.js └── views │ ├── index.ejs │ └── portable.ejs └── tests ├── client.test.js ├── server.test.js └── validKeys.js /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [ main, testing ] 9 | pull_request: 10 | branches: [ main ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [12.x, 14.x, 16.x] 20 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 21 | 22 | steps: 23 | - uses: actions/checkout@v2 24 | - name: Use Node.js ${{ matrix.node-version }} 25 | uses: actions/setup-node@v2 26 | with: 27 | node-version: ${{ matrix.node-version }} 28 | cache: 'npm' 29 | - run: npm ci 30 | - run: npm run build --if-present 31 | - run: npm run test-server 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | db 3 | testDB 4 | dist 5 | coverage 6 | *.DS_Store 7 | ManualServer.zip 8 | src/portable 9 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:16 2 | WORKDIR /usr/src/app/ 3 | COPY package*.json ./ 4 | COPY ./src ./src 5 | RUN npm install --production 6 | EXPOSE 3180 7 | CMD ["npm", "start"] 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FileDrop 2 | 3 | ![Banner](./src/assets/img/Banner.png) 4 | 5 | ![Downloads](https://www.xtrendence.dev/scripts/filedrop-stats.php?svg=true&custom=true) 6 | 7 | *Based on GitHub Release downloads and DockerHub pulls.* 8 | 9 | ### Disclaimer 10 | 11 | This was made as the coursework for my COMP3006 university module. The requirements were to develop a responsive and interactive web application that uses web sockets, Node.js, and PouchDB or MongoDB as its server-side database. This project got 89/100. 12 | 13 | ### What is FileDrop? 14 | 15 | FileDrop is an application that allows for encrypted file sharing between two users through the use of web sockets. 16 | 17 | ### How does it work? 18 | 19 | You download the application for the operating system you use from the [Releases](https://github.com/Xtrendence/FileDrop/releases) section, and run the app. Once you can see the IP and port of the server, it means it's working. You can then use any device on the same network to navigate to that IP and port. From there, you can choose a username and log in. Devices that are logged in would be able to see each other. At this point, you can ask for another client's permission to send them a file, or manually whitelist a client so they can send you a file whenever they wish. 20 | 21 | ### How can I host the server without using the Electron app? 22 | 23 | Electron apps are big, no arguments there. You can simply download the source code (or ManualServer.zip from the [Releases](https://github.com/Xtrendence/FileDrop/releases) section), open a terminal in the same directory as the `package.json` file, run `npm install` followed by `npm start`. Alternatively, you can use the [Docker image](https://hub.docker.com/r/xtrendence/filedrop) (further instructions can be found in the README on DockerHub). 24 | 25 | ### How does the encryption work? 26 | 27 | First and foremost, all encryption is done on the client-side so that the server doesn't need to be trusted. This also increases performance as encryption is a resource-intensive task, and by distributing the workload between clients, the server can transfer files as fast as possible. When the page first loads, an RSA public/private key pair is generated and stored in the browser's local storage. The public key is then broadcasted to other clients. When the user chooses a file and clicks on the upload button, the file is split into chunks of 256KB. A 256-bit AES key is then generated, and the public RSA key of the client the user is sending the file to is used to encrypt the AES key. The AES key is then used to symmetrically encrypt each chunk before sending it to the server. The encrypted AES key is sent with the encrypted chunk data as well. The other client then uses their private key to decrypt the AES key, which they can use to decrypt the chunk data. Once all the chunks have been received, they're put together to form the original file, which is then downloaded to the client's device. 28 | 29 | ### What does the app look like? 30 | 31 | **Login Page** 32 | 33 | ![Login](https://i.imgur.com/ttK0u1m.png) 34 | 35 | **Application Page** 36 | 37 | ![Application](https://i.imgur.com/7KbrnGv.png) 38 | 39 | ### Attributions 40 | 41 | |Resource |URL | 42 | |-----------------------------|------------------------------------------------------------| 43 | |Mockup Devices |[Device Shots](https://deviceshots.com/) | 44 | |Login Background |[BG Jar](https://bgjar.com/) | 45 | |Main Background |[SVG Backgrounds](https://www.svgbackgrounds.com/) | 46 | |Font Awesome |[Font Awesome](https://www.fontawesome.com/) | 47 | |Jest |[NPM](https://www.npmjs.com/package/jest) | 48 | |CORS |[NPM](https://www.npmjs.com/package/cors) | 49 | |EJS |[NPM](https://www.npmjs.com/package/ejs) | 50 | |Express |[NPM](https://www.npmjs.com/package/express) | 51 | |Jest |[NPM](https://www.npmjs.com/package/jest) | 52 | |Nodemon |[NPM](https://www.npmjs.com/package/nodemon) | 53 | |PouchDB |[NPM](https://www.npmjs.com/package/pouchdb) | 54 | |Socket.IO |[NPM](https://www.npmjs.com/package/socket.io) | 55 | |Socket.IO Client|[NPM](https://www.npmjs.com/package/socket.io-client) | 56 | |SuperTest |[NPM](https://www.npmjs.com/package/supertest) | 57 | |CryptoJS |[NPM](https://www.npmjs.com/package/crypto-js) | 58 | |Forge |[NPM](https://www.npmjs.com/package/forge) | 59 | |Puppeteer |[NPM](https://www.npmjs.com/package/puppeteer) | 60 | |Electron |[NPM](https://www.npmjs.com/package/electron) | 61 | |Electron Builder |[NPM](https://www.npmjs.com/package/electron-builder) | 62 | |Electron Local Shortcut |[NPM](https://www.npmjs.com/package/electron-localshortcut) | 63 | -------------------------------------------------------------------------------- /build/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Xtrendence/FileDrop/101477c21d017d1fdc201006827a4e8ba6b7d2bd/build/icon.icns -------------------------------------------------------------------------------- /build/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Xtrendence/FileDrop/101477c21d017d1fdc201006827a4e8ba6b7d2bd/build/icon.ico -------------------------------------------------------------------------------- /build/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Xtrendence/FileDrop/101477c21d017d1fdc201006827a4e8ba6b7d2bd/build/icon.png -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | filedrop: 5 | build: . 6 | container_name: filedrop 7 | restart: unless-stopped 8 | ports: 9 | - 3180:3180 10 | extra_hosts: 11 | - "host.docker.internal:host-gateway" 12 | -------------------------------------------------------------------------------- /docs/ActivityDiagram.drawio: -------------------------------------------------------------------------------- 1 | 7Vttc9o4EP41mft0Gb+AgY8pkF5u7mY6Je21nzoCL1gXW3IlOcD9+pNs+VVAnIRiyCQzmVirF1u7z7NaaZUrdxxtPjIUB39TH8Irx/I3V+7kynFs23blHyXZZhLPHWWCFcO+blQKZvg/0EJLSxPsA681FJSGAsd14YISAgtRkyHG6LrebEnD+ltjtAJDMFug0JT+g30RZFLXcgZlxR+AV0H+atvTE4xQ3lpPhQfIp+uKyJ1euWNGqcieos0YQqW9XDFZv9s9tcWXMSCiTYeod/dn0P+RLH7c3M9jazYQ3uR3PcojChM9Y/2xYpurQI4itS0LHwIRhVJmy0c5mVjVc4GYmAkkVP0Sh+GYhpSlHd3paDy+vVWNBaMPUKmx0h9Zo98OTMBm77TsQlkSZkAjEGwrm+gOXj/roQE21NpeV62ViYKKnXIZ0vhYFeOWGpQPWonPUKjTQqG+hJguUiYCuqIEhdNSWlW00g2WeLwJ8YpI2ZwKQSNZAcS/UQBXg8RAMokm0PCg0hlNiA9qElbaS077mypc9/Pi92rdZFMrbXUpm5aaS81wnCZsAU8jzjCwJDZiKxAHuvZ2A4FBiAR+rH/HLsPqrp8olm8uADSqA8geNKCRfZbu1EBH8RUvB4xrAGamOGWgRjJENDhYszChpMlBLUIaOgupcGA7MBVh309xtw6wgFmMUgOupUc30HIEwvatmr77nkFY75SE9V7Az7pOyjZ/URpr4/wLQmw1HVEiaN10sMHiW+X5e8k/WSoZpwo54Uqi1mhasnYPUZ/yvgaFd1DzSVb3WlK4/0oKv8rUPYNqH2kaUCisUa58gg+PeAG/8SvHCxXd5kw+rdTT3Sf1Sb7PgPNrAzMlIuw9PNpL3KpFGivoxJtO0hX0CLRz6rQr3FyFd7azg3hNd3g0awzfiXcE4vVbEm/QJfH6BvHutzJ8lL2Iep38TTgwgiIlk9qS8XyIFw+qoBqIQMklEX8mKlj+8BkRX8ZARVlWzhMZFpGczCsgwFRMqgaAy2TrudE135FVjPilNNo9egCSG++OyEZysA75XXK6iGVb8Pt5EfBJ+D24hIXVNoPYrxkCrBIj787+ZGCw9ziX06BhYIDBdMABjeYJP1Pn22vGSvkZQ8X5Dnf43uEv87398/Gldkv6nKEvtdseNNivPWl4nbXNQ7kb/qADJRkYQXp6sWQqBNKxUSENcbqNWabrcAwswpzjMjDiQHw9kOTPRUZGXu/sIqOhYa/P8DOB1BITIBjOKBS6ZPq2PWSwvS7p65iBcgmHm8UCYtExIF4SDtlnGA61B8SoU39unjtdWEDkeXWf61qmzz1tQOQ9rdKDeSuVK+k0azWsZx16XeetbDNqnxLTTb2ZLIQ3aKQhbNMAJ81DOO77mnCMNWHUck3IPEZna8LIoNs4oJRDJTpvBO3pcxbuU/mHFWH/JUbxPau5ophb7NNG8c77HvsY/MsX4qf51+keO//MKv8OpRy+xCFF/oGUAyZY4CzlkPXNGcwQ4Utgl0jSZhLiDEhqus3cMtYtkhrvdGP1Zs6ZnbYbK2fYJYld86CsQMMskTttUGZ1PBQpNpE5jws1XZKXv2SAuNZugGgPY1278icb66XXuPImdLnk8EuuaDkXv4Fv3v7o9TvOaLg79uvvjvr5PBy25GHHR6LmCfksoGv1KhVAAWNpviICztHqIrMSzf1Mz+s6K+F2ep7wHIbtN1jljrM+RZKSW6wUcegO8kuYtG+heuXlYq+Ji/y28aluF5vJCM08rgIkzt8W7bwdB9PHop0slv+skdmn/J8Xd/o/ -------------------------------------------------------------------------------- /docs/ActivityDiagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Xtrendence/FileDrop/101477c21d017d1fdc201006827a4e8ba6b7d2bd/docs/ActivityDiagram.png -------------------------------------------------------------------------------- /docs/ClientClassDiagram.drawio: -------------------------------------------------------------------------------- 1 | 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 -------------------------------------------------------------------------------- /docs/ClientClassDiagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Xtrendence/FileDrop/101477c21d017d1fdc201006827a4e8ba6b7d2bd/docs/ClientClassDiagram.png -------------------------------------------------------------------------------- /docs/SequenceDiagramLogin.drawio: -------------------------------------------------------------------------------- 1 | 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 -------------------------------------------------------------------------------- /docs/SequenceDiagramLogin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Xtrendence/FileDrop/101477c21d017d1fdc201006827a4e8ba6b7d2bd/docs/SequenceDiagramLogin.png -------------------------------------------------------------------------------- /docs/SequenceDiagramSendFile.drawio: -------------------------------------------------------------------------------- 1 | 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 -------------------------------------------------------------------------------- /docs/SequenceDiagramSendFile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Xtrendence/FileDrop/101477c21d017d1fdc201006827a4e8ba6b7d2bd/docs/SequenceDiagramSendFile.png -------------------------------------------------------------------------------- /docs/ServerClassDiagram.drawio: -------------------------------------------------------------------------------- 1 | 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 -------------------------------------------------------------------------------- /docs/ServerClassDiagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Xtrendence/FileDrop/101477c21d017d1fdc201006827a4e8ba6b7d2bd/docs/ServerClassDiagram.png -------------------------------------------------------------------------------- /docs/SystemArchitecture.drawio: -------------------------------------------------------------------------------- 1 | 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 -------------------------------------------------------------------------------- /docs/SystemArchitecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Xtrendence/FileDrop/101477c21d017d1fdc201006827a4e8ba6b7d2bd/docs/SystemArchitecture.png -------------------------------------------------------------------------------- /docs/UserPersona.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Xtrendence/FileDrop/101477c21d017d1fdc201006827a4e8ba6b7d2bd/docs/UserPersona.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | FileDrop 18 | 177 | 178 | 179 |
180 | FileDrop 181 |
182 |
183 | 184 |
185 |
186 | Download 187 |
188 |
189 |
190 | 191 |
192 | 193 |
194 |
195 | 196 |
197 | 198 |
199 |
200 |
201 |
202 |
203 | Cross-Platform 204 |
205 |
206 | 207 |
208 | 209 | 210 | 211 |
212 |
213 |
214 |
215 | Send Files Across Any Device 216 |
217 |
218 |
219 | 220 | Laptop (Login) 221 |
222 |
223 | 224 | Desktop 225 |
226 |
227 | 228 | Laptop (Application) 229 |
230 |
231 | 232 | Tablet 233 |
234 |
235 | 236 | Phone 237 |
238 |
239 | 240 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "typeAcquisition": { 3 | "include": [ 4 | "jest" 5 | ] 6 | } 7 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "filedrop", 3 | "version": "1.0.6", 4 | "description": "An encrypted file sharing application that leverages web sockets to allow users to send and receive files.", 5 | "main": "src/server.js", 6 | "scripts": { 7 | "test": "jest tests/server.test.js --verbose && jest tests/client.test.js --verbose", 8 | "test-async": "jest --detectOpenHandles --verbose", 9 | "test-coverage": "jest --coverage --verbose", 10 | "test-client": "jest tests/client.test.js --verbose", 11 | "test-server": "jest tests/server.test.js --verbose", 12 | "start": "node src/server.js", 13 | "dev": "nodemon src/server.js", 14 | "dist": "touch src/portable && sudo electron-builder -mwl && rm src/portable && npm run dist-manual", 15 | "dist-macos": "touch src/portable && sudo electron-builder -m --x64 --arm64 && rm src/portable", 16 | "dist-windows": "copy NUL > src\\portable && electron-builder -w && del src\\portable", 17 | "dist-linux": "touch src/portable && sudo electron-builder -l && rm src/portable", 18 | "dist-manual": "zip -9 -r ManualServer.zip src package.json package-lock.json README.md LICENSE.md jsconfig.json Dockerfile docker-compose.yml", 19 | "electron-start": "electron src/server.js portable", 20 | "electron-dev": "nodemon --exec electron src/server.js portable" 21 | }, 22 | "build": { 23 | "appId": "com.xtrendence.filedrop", 24 | "productName": "FileDrop", 25 | "files": [ 26 | "src/", 27 | "node_modules/", 28 | "package.json" 29 | ], 30 | "asar": false, 31 | "mac": { 32 | "target": [ 33 | "dmg" 34 | ], 35 | "artifactName": "FileDrop.${version}.Mac.${arch}.${ext}" 36 | }, 37 | "win": { 38 | "target": [ 39 | "nsis" 40 | ], 41 | "artifactName": "FileDrop.${version}.Windows.${ext}", 42 | "icon": "build/icon.ico" 43 | }, 44 | "linux": { 45 | "target": [ 46 | "AppImage", 47 | "deb" 48 | ], 49 | "artifactName": "FileDrop.${version}.Linux.${ext}" 50 | } 51 | }, 52 | "jest": { 53 | "testMatch": [ 54 | "**/tests/*.test.js" 55 | ] 56 | }, 57 | "repository": { 58 | "type": "git", 59 | "url": "git+https://github.com/Xtrendence/FileDrop.git" 60 | }, 61 | "author": "Xtrendence ", 62 | "license": "ISC", 63 | "bugs": { 64 | "url": "https://github.com/Xtrendence/FileDrop/issues" 65 | }, 66 | "homepage": "https://github.com/Xtrendence/FileDrop#readme", 67 | "dependencies": { 68 | "@types/jest": "^27.0.3", 69 | "cors": "^2.8.5", 70 | "ejs": "^3.1.6", 71 | "electron-localshortcut": "^3.2.1", 72 | "express": "^4.17.1", 73 | "nodemon": "^2.0.15", 74 | "pouchdb": "^7.2.2", 75 | "socket.io": "^4.4.0", 76 | "supertest": "^6.1.6" 77 | }, 78 | "devDependencies": { 79 | "electron": "^16.0.2", 80 | "electron-builder": "^22.14.5", 81 | "jest": "^27.3.1", 82 | "puppeteer": "^12.0.0", 83 | "socket.io-client": "^4.4.0" 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /promotional-assets/Desktop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Xtrendence/FileDrop/101477c21d017d1fdc201006827a4e8ba6b7d2bd/promotional-assets/Desktop.png -------------------------------------------------------------------------------- /promotional-assets/DockerHub.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Xtrendence/FileDrop/101477c21d017d1fdc201006827a4e8ba6b7d2bd/promotional-assets/DockerHub.png -------------------------------------------------------------------------------- /promotional-assets/GitHub.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Xtrendence/FileDrop/101477c21d017d1fdc201006827a4e8ba6b7d2bd/promotional-assets/GitHub.png -------------------------------------------------------------------------------- /promotional-assets/Laptop-Application.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Xtrendence/FileDrop/101477c21d017d1fdc201006827a4e8ba6b7d2bd/promotional-assets/Laptop-Application.png -------------------------------------------------------------------------------- /promotional-assets/Laptop-Login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Xtrendence/FileDrop/101477c21d017d1fdc201006827a4e8ba6b7d2bd/promotional-assets/Laptop-Login.png -------------------------------------------------------------------------------- /promotional-assets/Linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Xtrendence/FileDrop/101477c21d017d1fdc201006827a4e8ba6b7d2bd/promotional-assets/Linux.png -------------------------------------------------------------------------------- /promotional-assets/Phone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Xtrendence/FileDrop/101477c21d017d1fdc201006827a4e8ba6b7d2bd/promotional-assets/Phone.png -------------------------------------------------------------------------------- /promotional-assets/Tablet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Xtrendence/FileDrop/101477c21d017d1fdc201006827a4e8ba6b7d2bd/promotional-assets/Tablet.png -------------------------------------------------------------------------------- /promotional-assets/Windows.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Xtrendence/FileDrop/101477c21d017d1fdc201006827a4e8ba6b7d2bd/promotional-assets/Windows.png -------------------------------------------------------------------------------- /promotional-assets/macOS.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Xtrendence/FileDrop/101477c21d017d1fdc201006827a4e8ba6b7d2bd/promotional-assets/macOS.png -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- 1 | // Imports. 2 | const express = require("express"); 3 | const cors = require("cors"); 4 | const path = require("path"); 5 | const app = express(); 6 | 7 | // Use EJS. 8 | app.set("views", path.join(__dirname, "views")); 9 | app.set("view engine", "ejs"); 10 | app.use("/assets", express.static(path.join(__dirname, "assets"))); 11 | 12 | // Use CORS. 13 | app.use(cors()); 14 | 15 | // When the root route is requested, the index page is served. 16 | app.get("/", (request, response) => { 17 | response.render("index"); 18 | }); 19 | 20 | // The Forge library looks for the Prime Worker in "/forge/prime.worker.js", so the server serves the file from the assets directory for better file organization. 21 | app.get("/forge/prime.worker.js", (request, response) => { 22 | response.sendFile(path.join(__dirname, "./assets/js/prime.worker.min.js")); 23 | }); 24 | 25 | module.exports = app; -------------------------------------------------------------------------------- /src/assets/css/dark.css: -------------------------------------------------------------------------------- 1 | html.dark { 2 | --shadow-dark:0 6px 10px rgba(40,40,40,0.25); 3 | --shadow-darker:0 6px 10px rgba(40,40,40,0.4); 4 | --shadow-smooth:0 1px 1px rgba(0,0,0,0.08),0 2px 2px rgba(0,0,0,0.05),0 4px 4px rgba(0,0,0,0.06),0 8px 8px rgba(0,0,0,0.1); 5 | --shadow-accent:0 0 12px rgba(0,150,255,0.9); 6 | --main-first:rgb(20,20,20); 7 | --main-first-transparent:rgba(20,20,20,0.6); 8 | --main-second:rgb(30,30,30); 9 | --main-third:rgb(40,40,40); 10 | --main-third-transparent:rgba(40,40,40,0.9); 11 | --main-fourth:rgb(50,50,50); 12 | --main-fifth:rgb(60,60,60); 13 | --main-contrast:rgb(255,255,255); 14 | --main-contrast-light:rgb(245,245,245); 15 | --main-contrast-lightest:rgb(200,200,200); 16 | --accent-first:rgb(137,121,211); 17 | --accent-first-transparent:rgba(137,121,211,0.25); 18 | --accent-first-transparent-low:rgba(137,121,211,0.5); 19 | --accent-second:rgb(72,137,221); 20 | --accent-third:rgb(25,165,207); 21 | --accent-fourth:rgb(12,199,209); 22 | --accent-fifth:rgb(206,98,218); 23 | --accent-gradient:linear-gradient(15deg,var(--accent-first) 25%,var(--accent-second) 50%,var(--accent-third) 75%,var(--accent-fourth) 100%); 24 | --colorful-gradient:linear-gradient(300deg,#8a2387,#e94057,#f27121); 25 | --orange-gradient:linear-gradient(150deg,rgb(245,175,25) 30%,rgb(241,39,17) 100%); 26 | --green-gradient:linear-gradient(20deg,#67b26f,#4ca2cd); 27 | --calm-gradient:linear-gradient(20deg,#5f2c82,#49a09d); 28 | --blue-gradient:linear-gradient(to right,#4facfe 0%,#00bdc7 100%); 29 | --purple-gradient:linear-gradient(135deg,#667eea 0%,#764ba2 100%); 30 | --greener-gradient:linear-gradient(to right,#11998e,#38ef7d); 31 | --cosmic-gradient:linear-gradient(330deg,#ff00cc,#333399); 32 | --playful-gradient:linear-gradient(340deg,#fc00ff,#27bcbe); 33 | --atlas-gradient:linear-gradient(300deg,#feac5e,#c779d0,#4bc0c8); 34 | --accent-contrast:rgb(255,255,255); 35 | --border-radius:10px; 36 | --font-family:"Helvetica Neue","Lucida Grande","Arial","Verdana","Tahoma",sans-serif; 37 | } 38 | html.dark.reduce-border-radius { 39 | --border-radius:5px; 40 | } 41 | html.dark.no-border-radius { 42 | --border-radius:1px; 43 | } -------------------------------------------------------------------------------- /src/assets/css/light.css: -------------------------------------------------------------------------------- 1 | html.light { 2 | --shadow-dark:0 6px 10px rgba(40,40,40,0.25); 3 | --shadow-darker:0 6px 10px rgba(40,40,40,0.4); 4 | --shadow-smooth:0 1px 1px rgba(0,0,0,0.08),0 2px 2px rgba(0,0,0,0.05),0 4px 4px rgba(0,0,0,0.06),0 8px 8px rgba(0,0,0,0.1); 5 | --shadow-accent:0 0 12px rgba(0,150,255,0.9); 6 | --main-first:rgb(255,255,255); 7 | --main-first-transparent:rgba(255,255,255,0.7); 8 | --main-second:rgb(245,245,245); 9 | --main-third:rgb(235,235,235); 10 | --main-third-transparent:rgba(235,235,235,0.9); 11 | --main-fourth:rgb(225,225,225); 12 | --main-fifth:rgb(215,215,215); 13 | --main-contrast:rgb(50,50,50); 14 | --main-contrast-light:rgb(100,100,100); 15 | --main-contrast-lightest:rgb(150,150,150); 16 | --accent-first:rgb(137,121,211); 17 | --accent-first-transparent:rgba(137,121,211,0.25); 18 | --accent-first-transparent-low:rgba(137,121,211,0.5); 19 | --accent-second:rgb(72,137,221); 20 | --accent-third:rgb(25,165,207); 21 | --accent-fourth:rgb(12,199,209); 22 | --accent-fifth:rgb(206,98,218); 23 | --accent-gradient:linear-gradient(15deg,var(--accent-first) 25%,var(--accent-second) 50%,var(--accent-third) 75%,var(--accent-fourth) 100%); 24 | --colorful-gradient:linear-gradient(300deg,#8a2387,#e94057,#f27121); 25 | --orange-gradient:linear-gradient(150deg,rgb(245,175,25) 30%,rgb(241,39,17) 100%); 26 | --green-gradient:linear-gradient(20deg,#67b26f,#4ca2cd); 27 | --calm-gradient:linear-gradient(20deg,#5f2c82,#49a09d); 28 | --blue-gradient:linear-gradient(to right,#4facfe 0%,#00bdc7 100%); 29 | --purple-gradient:linear-gradient(135deg,#667eea 0%,#764ba2 100%); 30 | --greener-gradient:linear-gradient(to right,#11998e,#38ef7d); 31 | --cosmic-gradient:linear-gradient(330deg,#ff00cc,#333399); 32 | --playful-gradient:linear-gradient(340deg,#fc00ff,#27bcbe); 33 | --atlas-gradient:linear-gradient(300deg,#feac5e,#c779d0,#4bc0c8); 34 | --accent-contrast:rgb(255,255,255); 35 | --border-radius:10px; 36 | --font-family:"Helvetica Neue","Lucida Grande","Arial","Verdana","Tahoma",sans-serif; 37 | } 38 | html.light.reduce-border-radius { 39 | --border-radius:5px; 40 | } 41 | html.light.no-border-radius { 42 | --border-radius:1px; 43 | } -------------------------------------------------------------------------------- /src/assets/css/resize.css: -------------------------------------------------------------------------------- 1 | @media screen and (max-width:800px) { 2 | .server-button { 3 | height:120px; 4 | padding:0 20px 0 20px; 5 | } 6 | .server-button.logged-in { 7 | left:20px; 8 | } 9 | .server-button span { 10 | display:block; 11 | line-height:34px; 12 | } 13 | .server-button .separator, .server-button .separator-required { 14 | display:none; 15 | } 16 | .logout-button { 17 | display:none; 18 | } 19 | #settings-logout-button { 20 | display:block; 21 | } 22 | .upload-wrapper .title { 23 | width:calc(100% - 100px); 24 | left:40px; 25 | } 26 | } 27 | 28 | @media screen and (max-width:420px) { 29 | .server-button { 30 | width:calc(100% - 40px); 31 | } 32 | .settings-button { 33 | bottom:150px; 34 | } 35 | .settings-wrapper { 36 | bottom:150px; 37 | } 38 | } -------------------------------------------------------------------------------- /src/assets/css/style.css: -------------------------------------------------------------------------------- 1 | html { 2 | font-family:var(--font-family); 3 | } 4 | body { 5 | background:var(--main-third); 6 | color:var(--main-contrast); 7 | margin:0; 8 | padding:0; 9 | font-size:0; 10 | overflow:hidden; 11 | } 12 | input, button { 13 | outline:none; 14 | border:none; 15 | } 16 | div, span, input, button, textarea { 17 | transition:background 0.25s, color 0.25s, border-radius 0.25s; 18 | } 19 | a { 20 | text-decoration:none; 21 | } 22 | .noselect { 23 | -webkit-user-select:none; 24 | -khtml-user-select:none; 25 | -moz-user-select:none; 26 | -ms-user-select:none; 27 | user-select:none; 28 | outline:none; 29 | } 30 | .hidden { 31 | display:none; 32 | visibility:hidden; 33 | position:absolute; 34 | z-index:-1; 35 | } 36 | 37 | ::-webkit-scrollbar, div::-webkit-scrollbar { 38 | display:block; 39 | width:4px; 40 | height:10px; 41 | background:var(--main-second); 42 | } 43 | ::-webkit-scrollbar-thumb, div::-webkit-scrollbar-thumb { 44 | border-radius:5px; 45 | background:var(--main-fifth); 46 | } 47 | ::-webkit-scrollbar-thumb:hover, div::-webkit-scrollbar-thumb:hover { 48 | background:var(--main-fourth); 49 | } 50 | ::-webkit-scrollbar-track, div::-webkit-scrollbar-track { 51 | background:var(--main-second); 52 | } 53 | 54 | input::-webkit-outer-spin-button, input::-webkit-inner-spin-button { 55 | -webkit-appearance:none; 56 | margin:0; 57 | } 58 | input[type=number] { 59 | -moz-appearance:textfield; 60 | } 61 | 62 | .hidden { 63 | display:none; 64 | } 65 | 66 | .loading-screen { 67 | position:absolute; 68 | top:0; 69 | left:0; 70 | width:100%; 71 | height:100%; 72 | z-index:999; 73 | background:rgb(20,20,20); 74 | } 75 | .loading-overlay { 76 | display:none; 77 | pointer-events:none; 78 | top:0; 79 | left:0; 80 | width:100%; 81 | height:100%; 82 | position:absolute; 83 | background:rgb(20,20,20); 84 | transition:none; 85 | z-index:99; 86 | } 87 | .loading-overlay.active { 88 | display:block; 89 | pointer-events:all; 90 | } 91 | .loading-overlay span { 92 | width:100%; 93 | line-height:30px; 94 | font-size:24px; 95 | color:rgb(235,235,235); 96 | position:absolute; 97 | top:calc(50% - 30px / 2); 98 | text-align:center; 99 | display:block; 100 | position:absolute; 101 | } 102 | .loading-screen span { 103 | font-size:18px; 104 | font-weight:bold; 105 | color:rgb(255,255,255); 106 | display:block; 107 | text-align:center; 108 | width:100%; 109 | line-height:40px; 110 | position:absolute; 111 | top:calc(50% - 40px / 2 + 70px); 112 | pointer-events:none; 113 | } 114 | .loading-icon { 115 | display:inline-block; 116 | position:absolute; 117 | top:calc(50% - 80px / 2); 118 | left:calc(50% - 80px / 2); 119 | width:80px; 120 | height:80px; 121 | } 122 | .loading-icon div { 123 | position:absolute; 124 | border:4px solid #fff; 125 | opacity:1; 126 | border-radius:50%; 127 | animation:loading-icon 1s cubic-bezier(0, 0.2, 0.8, 1) infinite; 128 | } 129 | .loading-icon div:nth-child(2) { 130 | animation-delay:-0.5s; 131 | } 132 | @keyframes loading-icon { 133 | 0% { 134 | top:36px; 135 | left:36px; 136 | width:0; 137 | height:0; 138 | opacity:1; 139 | } 140 | 100% { 141 | top:0px; 142 | left:0px; 143 | width:72px; 144 | height:72px; 145 | opacity:0; 146 | } 147 | } 148 | 149 | .login-wrapper, .app-wrapper { 150 | position:absolute; 151 | opacity:1; 152 | top:0; 153 | left:0; 154 | z-index:1; 155 | height:100%; 156 | width:100%; 157 | transition:opacity 0.25s; 158 | } 159 | 160 | .login-wrapper { 161 | z-index:10; 162 | background-size:cover; 163 | background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' version='1.1' xmlns:xlink='http://www.w3.org/1999/xlink' xmlns:svgjs='http://svgjs.com/svgjs' width='3840' height='2160' preserveAspectRatio='none' viewBox='0 0 3840 2160'%3e%3cg mask='url(%26quot%3b%23SvgjsMask1009%26quot%3b)' fill='none'%3e%3crect width='3840' height='2160' x='0' y='0' fill='url(%23SvgjsLinearGradient1010)'%3e%3c/rect%3e%3cuse xlink:href='%23SvgjsSymbol1017' x='0' y='0'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsSymbol1017' x='0' y='720'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsSymbol1017' x='0' y='1440'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsSymbol1017' x='720' y='0'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsSymbol1017' x='720' y='720'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsSymbol1017' x='720' y='1440'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsSymbol1017' x='1440' y='0'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsSymbol1017' x='1440' y='720'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsSymbol1017' x='1440' y='1440'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsSymbol1017' x='2160' y='0'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsSymbol1017' x='2160' y='720'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsSymbol1017' x='2160' y='1440'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsSymbol1017' x='2880' y='0'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsSymbol1017' x='2880' y='720'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsSymbol1017' x='2880' y='1440'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsSymbol1017' x='3600' y='0'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsSymbol1017' x='3600' y='720'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsSymbol1017' x='3600' y='1440'%3e%3c/use%3e%3c/g%3e%3cdefs%3e%3cmask id='SvgjsMask1009'%3e%3crect width='3840' height='2160' fill='white'%3e%3c/rect%3e%3c/mask%3e%3clinearGradient x1='10.94%25' y1='-19.44%25' x2='89.06%25' y2='119.44%25' gradientUnits='userSpaceOnUse' id='SvgjsLinearGradient1010'%3e%3cstop stop-color='rgba(181%2c 85%2c 202%2c 1)' offset='0'%3e%3c/stop%3e%3cstop stop-color='rgba(119%2c 115%2c 220%2c 1)' offset='0.48'%3e%3c/stop%3e%3cstop stop-color='rgba(77%2c 185%2c 197%2c 1)' offset='1'%3e%3c/stop%3e%3c/linearGradient%3e%3cpath d='M-1 0 a1 1 0 1 0 2 0 a1 1 0 1 0 -2 0z' id='SvgjsPath1014'%3e%3c/path%3e%3cpath d='M-3 0 a3 3 0 1 0 6 0 a3 3 0 1 0 -6 0z' id='SvgjsPath1015'%3e%3c/path%3e%3cpath d='M-5 0 a5 5 0 1 0 10 0 a5 5 0 1 0 -10 0z' id='SvgjsPath1011'%3e%3c/path%3e%3cpath d='M2 -2 L-2 2z' id='SvgjsPath1013'%3e%3c/path%3e%3cpath d='M6 -6 L-6 6z' id='SvgjsPath1012'%3e%3c/path%3e%3cpath d='M30 -30 L-30 30z' id='SvgjsPath1016'%3e%3c/path%3e%3c/defs%3e%3csymbol id='SvgjsSymbol1017'%3e%3cuse xlink:href='%23SvgjsPath1011' x='30' y='30' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1012' x='30' y='90' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1013' x='30' y='150' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1013' x='30' y='210' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1012' x='30' y='270' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1011' x='30' y='330' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1014' x='30' y='390' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1012' x='30' y='450' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1012' x='30' y='510' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1012' x='30' y='570' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1013' x='30' y='630' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1015' x='30' y='690' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1012' x='90' y='30' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1012' x='90' y='90' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1013' x='90' y='150' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1011' x='90' y='210' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1011' x='90' y='270' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1013' x='90' y='330' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1013' x='90' y='390' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1016' x='90' y='450' stroke='rgba(235%2c 235%2c 235%2c 1)' stroke-width='3'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1011' x='90' y='510' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1016' x='90' y='570' stroke='rgba(235%2c 235%2c 235%2c 1)' stroke-width='3'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1014' x='90' y='630' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1011' x='90' y='690' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1011' x='150' y='30' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1015' x='150' y='90' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1011' x='150' y='150' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1015' x='150' y='210' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1011' x='150' y='270' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1012' x='150' y='330' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1012' x='150' y='390' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1013' x='150' y='450' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1012' x='150' y='510' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1015' x='150' y='570' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1011' x='150' y='630' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1013' x='150' y='690' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1012' x='210' y='30' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1011' x='210' y='90' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1011' x='210' y='150' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1012' x='210' y='210' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1012' x='210' y='270' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1012' x='210' y='330' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1012' x='210' y='390' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1012' x='210' y='450' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1012' x='210' y='510' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1011' x='210' y='570' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1011' x='210' y='630' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1012' x='210' y='690' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1012' x='270' y='30' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1015' x='270' y='90' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1011' x='270' y='150' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1012' x='270' y='210' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1014' x='270' y='270' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1012' x='270' y='330' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1015' x='270' y='390' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1011' x='270' y='450' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1012' x='270' y='510' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1014' x='270' y='570' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1014' x='270' y='630' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1012' x='270' y='690' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1011' x='330' y='30' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1012' x='330' y='90' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1011' x='330' y='150' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1016' x='330' y='210' stroke='rgba(235%2c 235%2c 235%2c 1)' stroke-width='3'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1012' x='330' y='270' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1011' x='330' y='330' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1012' x='330' y='390' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1014' x='330' y='450' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1015' x='330' y='510' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1015' x='330' y='570' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1014' x='330' y='630' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1014' x='330' y='690' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1011' x='390' y='30' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1014' x='390' y='90' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1015' x='390' y='150' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1011' x='390' y='210' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1011' x='390' y='270' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1015' x='390' y='330' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1011' x='390' y='390' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1011' x='390' y='450' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1012' x='390' y='510' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1016' x='390' y='570' stroke='rgba(235%2c 235%2c 235%2c 1)' stroke-width='3'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1012' x='390' y='630' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1015' x='390' y='690' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1013' x='450' y='30' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1013' x='450' y='90' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1011' x='450' y='150' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1014' x='450' y='210' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1013' x='450' y='270' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1012' x='450' y='330' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1013' x='450' y='390' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1012' x='450' y='450' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1012' x='450' y='510' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1012' x='450' y='570' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1012' x='450' y='630' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1011' x='450' y='690' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1012' x='510' y='30' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1011' x='510' y='90' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1012' x='510' y='150' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1013' x='510' y='210' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1015' x='510' y='270' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1012' x='510' y='330' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1011' x='510' y='390' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1012' x='510' y='450' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1013' x='510' y='510' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1012' x='510' y='570' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1012' x='510' y='630' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1011' x='510' y='690' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1015' x='570' y='30' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1011' x='570' y='90' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1011' x='570' y='150' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1012' x='570' y='210' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1012' x='570' y='270' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1012' x='570' y='330' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1013' x='570' y='390' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1016' x='570' y='450' stroke='rgba(235%2c 235%2c 235%2c 1)' stroke-width='3'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1016' x='570' y='510' stroke='rgba(235%2c 235%2c 235%2c 1)' stroke-width='3'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1015' x='570' y='570' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1011' x='570' y='630' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1012' x='570' y='690' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1015' x='630' y='30' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1011' x='630' y='90' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1014' x='630' y='150' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1012' x='630' y='210' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1016' x='630' y='270' stroke='rgba(235%2c 235%2c 235%2c 1)' stroke-width='3'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1015' x='630' y='330' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1014' x='630' y='390' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1014' x='630' y='450' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1012' x='630' y='510' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1012' x='630' y='570' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1012' x='630' y='630' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1014' x='630' y='690' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1016' x='690' y='30' stroke='rgba(235%2c 235%2c 235%2c 1)' stroke-width='3'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1011' x='690' y='90' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1012' x='690' y='150' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1014' x='690' y='210' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1011' x='690' y='270' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1011' x='690' y='330' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1015' x='690' y='390' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1012' x='690' y='450' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1014' x='690' y='510' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1012' x='690' y='570' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1012' x='690' y='630' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3cuse xlink:href='%23SvgjsPath1012' x='690' y='690' stroke='rgba(235%2c 235%2c 235%2c 1)'%3e%3c/use%3e%3c/symbol%3e%3c/svg%3e"); 164 | } 165 | .login-wrapper svg { 166 | width:100%; 167 | height:100%; 168 | } 169 | .login-title { 170 | position:absolute; 171 | z-index:11; 172 | top:20px; 173 | left:20px; 174 | font-size:20px; 175 | padding:5px 10px 5px 10px; 176 | border-radius:var(--border-radius); 177 | background:var(--main-first-transparent); 178 | backdrop-filter:blur(2px); 179 | font-weight:bold; 180 | color:var(--main-contrast-light); 181 | box-shadow:var(--shadow-smooth); 182 | } 183 | 184 | .login-container { 185 | z-index:2; 186 | overflow:hidden; 187 | border-radius:var(--border-radius); 188 | background:var(--main-first-transparent); 189 | box-shadow:var(--shadow-smooth); 190 | backdrop-filter:blur(2px); 191 | position:absolute; 192 | width:260px; 193 | height:104px; 194 | left:calc(50% - 260px / 2); 195 | top:calc(50% - 104px / 2); 196 | } 197 | .login-container input { 198 | display:block; 199 | border-radius:var(--border-radius); 200 | margin:10px auto 10px auto; 201 | width:calc(100% - 20px - 20px); 202 | padding:0 10px 0 10px; 203 | height:36px; 204 | background:var(--main-first); 205 | font-size:16px; 206 | font-weight:bold; 207 | color:var(--main-contrast-light); 208 | box-shadow:var(--shadow-smooth); 209 | } 210 | .login-container input::placeholder { 211 | color:var(--main-contrast-lightest); 212 | } 213 | .login-container div { 214 | display:block; 215 | margin:0 auto 0 auto; 216 | padding-left:10px; 217 | } 218 | .login-container div button { 219 | cursor:pointer; 220 | margin-right:10px; 221 | width:calc(50% - 10px); 222 | height:36px; 223 | border-radius:var(--border-radius); 224 | font-weight:bold; 225 | background:var(--main-first); 226 | color:var(--main-contrast-light); 227 | box-shadow:var(--shadow-smooth); 228 | } 229 | #desktop .login-container div button:hover { 230 | background:var(--accent-first); 231 | color:var(--accent-contrast); 232 | } 233 | 234 | .app-wrapper #background { 235 | position:fixed; 236 | z-index:1; 237 | top:0; 238 | left:0; 239 | min-width:100%; 240 | min-height:100%; 241 | } 242 | 243 | .round-button { 244 | cursor:pointer; 245 | height:48px; 246 | width:48px; 247 | padding:0; 248 | border-radius:50%; 249 | background:var(--main-first-transparent); 250 | backdrop-filter:blur(2px); 251 | } 252 | .round-button svg { 253 | margin-top:2px; 254 | width:32px; 255 | height:32px; 256 | fill:var(--main-contrast-light); 257 | } 258 | .round-button:hover { 259 | background:var(--main-first); 260 | } 261 | 262 | .settings-button { 263 | z-index:11; 264 | bottom:20px; 265 | right:20px; 266 | position:absolute; 267 | box-shadow:var(--shadow-smooth); 268 | } 269 | 270 | .settings-wrapper, .clients-wrapper { 271 | z-index:11; 272 | bottom:20px; 273 | right:-220px; 274 | width:220px; 275 | height:auto; 276 | max-height:calc(100% - 160px); 277 | transition:right 0.25s ease-in-out; 278 | overflow:hidden auto; 279 | border-radius:var(--border-radius); 280 | position:absolute; 281 | background:var(--main-first-transparent); 282 | backdrop-filter:blur(2px); 283 | box-shadow:var(--shadow-smooth); 284 | } 285 | .clients-wrapper { 286 | right:20px; 287 | bottom:auto; 288 | top:20px; 289 | width:auto; 290 | max-height:300px; 291 | max-width:calc(100% - 40px); 292 | } 293 | .settings-wrapper .section, .clients-wrapper .client { 294 | display:block; 295 | background:var(--main-second); 296 | margin:10px; 297 | box-shadow:var(--shadow-smooth); 298 | border-radius:var(--border-radius); 299 | padding:10px; 300 | } 301 | .clients-wrapper .client .client-action { 302 | cursor:pointer; 303 | vertical-align:top; 304 | height:36px; 305 | padding:0 10px 0 10px; 306 | border-radius:var(--border-radius); 307 | font-weight:bold; 308 | float:right; 309 | background:var(--main-fourth); 310 | color:var(--main-contrast-light); 311 | box-shadow:var(--shadow-smooth); 312 | } 313 | #desktop .clients-wrapper .client .client-action:hover { 314 | background:var(--accent-first); 315 | color:var(--accent-contrast); 316 | } 317 | .clients-wrapper .client .client-info { 318 | cursor:pointer; 319 | vertical-align:top; 320 | height:36px; 321 | width:36px; 322 | padding:0; 323 | margin-left:10px; 324 | border-radius:var(--border-radius); 325 | font-weight:bold; 326 | float:right; 327 | background:var(--main-fourth); 328 | color:var(--main-contrast-light); 329 | box-shadow:var(--shadow-smooth); 330 | } 331 | #desktop .clients-wrapper .client .client-info:hover { 332 | background:var(--accent-first); 333 | color:var(--accent-contrast); 334 | } 335 | .clients-wrapper .client .client-info svg { 336 | margin-top:2px; 337 | width:28px; 338 | height:28px; 339 | fill:var(--main-contrast); 340 | } 341 | .settings-wrapper .section .title, .clients-wrapper .client span { 342 | display:inline-block; 343 | font-size:16px; 344 | font-weight:bold; 345 | color:var(--main-contrast-light); 346 | vertical-align:top; 347 | line-height:25px; 348 | } 349 | .clients-wrapper .client span { 350 | line-height:34px; 351 | margin-right:20px; 352 | } 353 | .settings-wrapper .section .toggle-wrapper { 354 | float:right; 355 | } 356 | .toggle-wrapper { 357 | width:50px; 358 | height:25px; 359 | background:var(--main-third); 360 | border-radius:12.5px; 361 | position:relative; 362 | cursor:pointer; 363 | display:inline-block; 364 | box-shadow:var(--shadow-smooth); 365 | } 366 | .toggle-wrapper.active { 367 | background:var(--accent-first); 368 | } 369 | .toggle-container { 370 | width:25px; 371 | height:25px; 372 | border-radius:50%; 373 | background:var(--main-fifth); 374 | transition:0.25s; 375 | left:0; 376 | top:0; 377 | position:absolute; 378 | } 379 | .toggle-wrapper.active .toggle-container { 380 | left:25px; 381 | background:var(--main-fourth); 382 | } 383 | .toggle-container svg { 384 | width:15px; 385 | height:15px; 386 | padding:5px; 387 | fill:var(--accent-first); 388 | } 389 | .toggle-container .moon-icon { 390 | display:block; 391 | } 392 | .toggle-wrapper.active .moon-icon { 393 | display:none; 394 | } 395 | .toggle-wrapper.active .sun-icon { 396 | display:block; 397 | } 398 | .sun-icon { 399 | display:none; 400 | } 401 | .settings-wrapper .section-button { 402 | display:block; 403 | background:var(--main-second); 404 | margin:10px; 405 | width:calc(100% - 20px); 406 | height:45px; 407 | font-size:16px; 408 | cursor:pointer; 409 | box-shadow:var(--shadow-smooth); 410 | border-radius:var(--border-radius); 411 | color:var(--main-contrast); 412 | font-weight:bold; 413 | padding:0; 414 | } 415 | #desktop .settings-wrapper .section-button:hover { 416 | background:var(--main-third); 417 | } 418 | #settings-logout-button { 419 | display:none; 420 | } 421 | 422 | .upload-wrapper { 423 | z-index:12; 424 | left:0; 425 | top:0; 426 | width:100%; 427 | height:100%; 428 | overflow:hidden; 429 | position:absolute; 430 | background:rgba(20,20,20,0.6); 431 | backdrop-filter:blur(5px); 432 | } 433 | .upload-wrapper .title { 434 | position:absolute; 435 | z-index:14; 436 | top:20px; 437 | left:calc(50% - 600px / 2); 438 | display:block; 439 | text-align:center; 440 | line-height:30px; 441 | width:600px; 442 | font-size:20px; 443 | padding:5px 10px 5px 10px; 444 | border-radius:var(--border-radius); 445 | background:var(--accent-first); 446 | backdrop-filter:blur(2px); 447 | font-weight:bold; 448 | color:var(--accent-contrast); 449 | box-shadow:var(--shadow-smooth); 450 | } 451 | .upload-area { 452 | top:40px; 453 | left:20px; 454 | z-index:13; 455 | position:absolute; 456 | width:calc(100% - 40px); 457 | height:calc(100% - 40px - 80px); 458 | background:var(--accent-first-transparent); 459 | border-radius:var(--border-radius); 460 | border-style:dashed; 461 | border-width:2px; 462 | cursor:pointer; 463 | border-color:var(--accent-first); 464 | } 465 | .upload-area.disabled { 466 | background:var(--main-first-transparent); 467 | pointer-events:none; 468 | border:none; 469 | } 470 | #desktop .upload-area:hover { 471 | background:var(--accent-first-transparent-low); 472 | } 473 | .progress-wrapper { 474 | height:40px; 475 | width:200px; 476 | top:calc(50% - 40px / 2); 477 | left:calc(50% - 200px / 2); 478 | position:absolute; 479 | z-index:14; 480 | overflow:hidden; 481 | border-radius:20px; 482 | margin-bottom:10px; 483 | } 484 | #progress-background { 485 | background:rgb(20,20,20); 486 | height:100%; 487 | width:100%; 488 | position:absolute; 489 | top:0; 490 | left:0; 491 | z-index:2; 492 | } 493 | #progress-foreground { 494 | background:var(--blue-gradient); 495 | background-size:1400% 1400%; 496 | -webkit-animation:load-animation 4s ease infinite; 497 | -moz-animation:load-animation 4s ease infinite; 498 | -o-animation:load-animation 4s ease infinite; 499 | animation:load-animation 4s ease infinite; 500 | height:100%; 501 | width:50%; 502 | position:absolute; 503 | top:0; 504 | left:0; 505 | z-index:3; 506 | } 507 | .upload-area span { 508 | position:absolute; 509 | text-align:center; 510 | width:300px; 511 | line-height:30px; 512 | top:calc(50% - 30px / 2); 513 | left:calc(50% - 300px / 2); 514 | z-index:14; 515 | pointer-events:none; 516 | font-size:18px; 517 | font-weight:bold; 518 | color:var(--accent-contrast); 519 | } 520 | .upload-cancel-button, .upload-button { 521 | cursor:pointer; 522 | position:absolute; 523 | z-index:14; 524 | bottom:20px; 525 | left:calc(50% - 100px / 2 - 60px); 526 | font-size:20px; 527 | height:35px; 528 | width:100px; 529 | padding:5px 10px 5px 10px; 530 | border-radius:var(--border-radius); 531 | background:rgb(220,40,20); 532 | transition:opacity 0.25s; 533 | backdrop-filter:blur(2px); 534 | font-weight:bold; 535 | color:var(--accent-contrast); 536 | box-shadow:var(--shadow-smooth); 537 | } 538 | .upload-button { 539 | transition:background 0.25s; 540 | background:var(--accent-first); 541 | left:calc(50% - 100px / 2 + 60px); 542 | } 543 | #desktop .upload-cancel-button:hover { 544 | opacity:0.8; 545 | } 546 | #desktop .upload-button:hover { 547 | background:var(--accent-first-transparent); 548 | } 549 | 550 | .server-button, .logout-button { 551 | cursor:pointer; 552 | position:absolute; 553 | z-index:2; 554 | bottom:20px; 555 | left:20px; 556 | font-size:20px; 557 | height:35px; 558 | width:90px; 559 | padding:5px 10px 5px 10px; 560 | border-radius:var(--border-radius); 561 | background:var(--main-first-transparent); 562 | backdrop-filter:blur(2px); 563 | font-weight:bold; 564 | color:var(--main-contrast-light); 565 | box-shadow:var(--shadow-smooth); 566 | } 567 | #desktop .server-button:hover, #desktop .logout-button:hover { 568 | background:var(--main-first); 569 | color:var(--main-contrast-light); 570 | } 571 | .server-button { 572 | z-index:11; 573 | width:auto; 574 | top:auto; 575 | bottom:20px; 576 | background:var(--main-first-transparent); 577 | color:var(--main-contrast-light); 578 | transition:background 0.25s; 579 | } 580 | .server-button.logged-in { 581 | left:120px; 582 | } 583 | .server-button.active { 584 | color:var(--main-contrast-light); 585 | background:var(--main-first-transparent); 586 | } 587 | .server-button.active .status { 588 | color:var(--accent-first); 589 | } 590 | .server-button.processing { 591 | color:var(--accent-contrast); 592 | background:var(--accent-first); 593 | } 594 | .server-button .separator, .server-button .separator-required { 595 | padding:0 10px 0 10px; 596 | margin-top:-1px; 597 | display:inline-block; 598 | vertical-align:top; 599 | } 600 | 601 | .x-notification { 602 | box-shadow:var(--shadow-smooth); 603 | } 604 | .x-notification button { 605 | color:var(--accent-contrast); 606 | font-weight:bold; 607 | border:none; 608 | outline:none; 609 | height:36px; 610 | padding:0 15px 0 15px; 611 | border-radius:var(--border-radius); 612 | display:inline-block; 613 | cursor:pointer; 614 | opacity:1; 615 | transition:opacity 0.25s; 616 | } 617 | #desktop .x-notification button:hover { 618 | opacity:0.8; 619 | } 620 | .x-notification .live-span { 621 | font-size:16px; 622 | position:absolute; 623 | right:16px; 624 | bottom:16px; 625 | color:var(--accent-contrast); 626 | } 627 | .x-notification .buttons { 628 | text-align:right; 629 | padding-top:10px; 630 | } 631 | .x-notification .decline { 632 | background:rgb(220,40,20); 633 | } 634 | .x-notification .accept { 635 | margin-left:10px; 636 | background:var(--accent-first); 637 | } 638 | 639 | @-webkit-keyframes load-animation { 640 | 0% { 641 | background-position:7% 0% 642 | } 643 | 50% { 644 | background-position:94% 100% 645 | } 646 | 100% { 647 | background-position:7% 0% 648 | } 649 | } 650 | @-moz-keyframes load-animation { 651 | 0% { 652 | background-position:7% 0% 653 | } 654 | 50% { 655 | background-position:94% 100% 656 | } 657 | 100% { 658 | background-position:7% 0% 659 | } 660 | } 661 | @-o-keyframes load-animation { 662 | 0% { 663 | background-position:7% 0% 664 | } 665 | 50% { 666 | background-position:94% 100% 667 | } 668 | 100% { 669 | background-position:7% 0% 670 | } 671 | } 672 | @keyframes load-animation { 673 | 0% { 674 | background-position:7% 0% 675 | } 676 | 50% { 677 | background-position:94% 100% 678 | } 679 | 100% { 680 | background-position:7% 0% 681 | } 682 | } 683 | 684 | body.portable { 685 | background:var(--main-first-transparent); 686 | } 687 | .titlebar { 688 | position:absolute; 689 | width:100%; 690 | height:36px; 691 | top:0; 692 | left:0; 693 | background:var(--main-third-transparent); 694 | backdrop-filter:blur(2px); 695 | box-shadow:var(--shadow-smooth); 696 | -webkit-app-region:drag; 697 | } 698 | .titlebar img { 699 | position:absolute; 700 | width:28px; 701 | height:28px; 702 | top:4px; 703 | right:4px; 704 | } 705 | .window-buttons { 706 | font-size:0; 707 | height:100%; 708 | display:inline-block; 709 | vertical-align:top; 710 | } 711 | .window-buttons button { 712 | display:inline-block; 713 | width:18px; 714 | height:18px; 715 | margin:9px 0 0 9px; 716 | padding:0; 717 | border-radius:50%; 718 | cursor:default; 719 | vertical-align:top; 720 | -webkit-app-region:no-drag; 721 | position:relative; 722 | } 723 | .window-buttons button:hover svg { 724 | opacity:1; 725 | } 726 | .window-buttons button.close-button { 727 | background:var(--purple-gradient); 728 | } 729 | .window-buttons button.minimize-button { 730 | background:var(--blue-gradient); 731 | } 732 | .window-buttons button svg { 733 | fill:rgb(255,255,255); 734 | width:70%; 735 | height:70%; 736 | position:absolute; 737 | top:2px; 738 | left:15%; 739 | opacity:0; 740 | transition:opacity 0.25s; 741 | } 742 | .portable-wrapper { 743 | z-index:2; 744 | position:absolute; 745 | top:36px; 746 | left:0; 747 | width:100%; 748 | height:calc(100% - 36px); 749 | backdrop-filter:blur(2px); 750 | } 751 | .portable-wrapper span { 752 | display:block; 753 | background:var(--main-second); 754 | margin:10px; 755 | width:calc(100% - 20px); 756 | line-height:45px; 757 | font-size:16px; 758 | text-align:center; 759 | box-shadow:var(--shadow-smooth); 760 | border-radius:var(--border-radius); 761 | color:var(--main-contrast); 762 | font-weight:bold; 763 | padding:0; 764 | } 765 | .portable-wrapper button { 766 | display:block; 767 | background:var(--main-second); 768 | margin:10px; 769 | width:calc(100% - 20px); 770 | height:45px; 771 | font-size:16px; 772 | cursor:pointer; 773 | box-shadow:var(--shadow-smooth); 774 | border-radius:var(--border-radius); 775 | color:var(--main-contrast); 776 | font-weight:bold; 777 | padding:0; 778 | } 779 | .portable-wrapper button:hover { 780 | background:var(--main-third); 781 | } -------------------------------------------------------------------------------- /src/assets/img/Banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Xtrendence/FileDrop/101477c21d017d1fdc201006827a4e8ba6b7d2bd/src/assets/img/Banner.png -------------------------------------------------------------------------------- /src/assets/img/Icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Xtrendence/FileDrop/101477c21d017d1fdc201006827a4e8ba6b7d2bd/src/assets/img/Icon.png -------------------------------------------------------------------------------- /src/assets/img/Icon.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Xtrendence/FileDrop/101477c21d017d1fdc201006827a4e8ba6b7d2bd/src/assets/img/Icon.psd -------------------------------------------------------------------------------- /src/assets/img/favicon/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Xtrendence/FileDrop/101477c21d017d1fdc201006827a4e8ba6b7d2bd/src/assets/img/favicon/android-chrome-192x192.png -------------------------------------------------------------------------------- /src/assets/img/favicon/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Xtrendence/FileDrop/101477c21d017d1fdc201006827a4e8ba6b7d2bd/src/assets/img/favicon/android-chrome-512x512.png -------------------------------------------------------------------------------- /src/assets/img/favicon/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Xtrendence/FileDrop/101477c21d017d1fdc201006827a4e8ba6b7d2bd/src/assets/img/favicon/apple-touch-icon.png -------------------------------------------------------------------------------- /src/assets/img/favicon/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #9790ad 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/assets/img/favicon/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Xtrendence/FileDrop/101477c21d017d1fdc201006827a4e8ba6b7d2bd/src/assets/img/favicon/favicon-16x16.png -------------------------------------------------------------------------------- /src/assets/img/favicon/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Xtrendence/FileDrop/101477c21d017d1fdc201006827a4e8ba6b7d2bd/src/assets/img/favicon/favicon-32x32.png -------------------------------------------------------------------------------- /src/assets/img/favicon/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Xtrendence/FileDrop/101477c21d017d1fdc201006827a4e8ba6b7d2bd/src/assets/img/favicon/favicon.ico -------------------------------------------------------------------------------- /src/assets/img/favicon/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Xtrendence/FileDrop/101477c21d017d1fdc201006827a4e8ba6b7d2bd/src/assets/img/favicon/mstile-150x150.png -------------------------------------------------------------------------------- /src/assets/img/favicon/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.14, written by Peter Selinger 2001-2017 9 | 10 | 12 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/assets/img/favicon/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "FileDrop", 3 | "short_name": "FileDrop", 4 | "icons": [ 5 | { 6 | "src": "android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#c8c5de", 17 | "background_color": "#c8c5de", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /src/assets/js/ChunkReader.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A class that acts similarly to the FileReader, but reads data in chunks of ArrayBuffers. 3 | */ 4 | class ChunkReader { 5 | /** 6 | * @param {Object} file - The file to read. 7 | * @param {Number} chunkSize - The size of each chunk. 8 | * @param {Number} currentChunk - The number of the current chunk being read. 9 | * @param {Number} offset - Where in the file to start reading chunks from. 10 | * @returns {void} 11 | * @constructor 12 | */ 13 | constructor(file, chunkSize = 256 * 100, currentChunk = 0, offset = 0) { 14 | this.encryption = false; 15 | this.file = file; 16 | this.chunkSize = chunkSize; 17 | this.currentChunk = currentChunk; 18 | this.offset = offset; 19 | this.events = {}; 20 | this.stop = false; 21 | } 22 | 23 | /** 24 | * Creates a new FileReader instance, and sets the "onload" property which listens to the FileReader's load event as each chunk is read. If encryption is enabled, then the data is encrypted. The "chunkData" and "done" event listeners are used to pass the data back. 25 | * @returns {void} 26 | */ 27 | createReader() { 28 | this.reader = new FileReader(); 29 | 30 | this.reader.onload = async (event) => { 31 | if(!this.stop) { 32 | let content = event.target.result; 33 | 34 | let data = { chunkData:content, chunk:this.currentChunk, offset:this.offset }; 35 | 36 | if(this.encryption) { 37 | let encoded = CryptoFD.encode(content); 38 | let encrypted = CryptoFD.encryptAES(encoded, this.key); 39 | 40 | data = { chunkData:encrypted, key:this.encryptedKey, chunk:this.currentChunk, offset:this.offset }; 41 | } 42 | 43 | if(this.hasEvent("chunkData")) { 44 | this.events["chunkData"](data); 45 | } 46 | 47 | if(this.offset < this.file.size) { 48 | this.nextChunk(); 49 | } else { 50 | if(this.hasEvent("done")) { 51 | this.events["done"](this.encryption, this.file.name); 52 | } 53 | } 54 | } 55 | } 56 | 57 | this.reader.onerror = (error) => { 58 | if(this.hasEvent("error")) { 59 | this.events["error"](error); 60 | } 61 | }; 62 | } 63 | 64 | /** 65 | * Checks if an event exists. 66 | * @param {string} event - The name of the event. 67 | * @returns {Boolean} 68 | */ 69 | hasEvent(event) { 70 | return Object.keys(this.events).includes(event); 71 | } 72 | 73 | /** 74 | * Adds an event listener. 75 | * @param {string} event - The name of the event. 76 | * @returns {void} 77 | */ 78 | on(event, callback) { 79 | this.events[event] = callback; 80 | } 81 | 82 | /** 83 | * Removes an event listener. 84 | * @param {string} event - The name of the event. 85 | * @returns {void} 86 | */ 87 | off(event) { 88 | delete this.events[event]; 89 | } 90 | 91 | /** 92 | * Reads the next chunk from the specified file, and returns a progress update using the "nextChunk" event if it exists. 93 | * @returns {void} 94 | */ 95 | nextChunk() { 96 | if(!this.stop) { 97 | this.currentChunk++; 98 | 99 | let chunk = this.file.slice(this.offset, Math.min(this.offset + this.chunkSize, this.file.size)); 100 | 101 | this.reader.readAsArrayBuffer(chunk); 102 | 103 | this.offset += this.chunkSize; 104 | 105 | let percentage = Math.floor((this.offset / this.file.size * 100)); 106 | if(percentage > 100) { 107 | percentage = 100; 108 | } 109 | 110 | if(this.hasEvent("nextChunk")) { 111 | this.events["nextChunk"](percentage, this.currentChunk, this.offset); 112 | } 113 | } 114 | } 115 | 116 | /** 117 | * Generates a random AES key, and encrypts it with the recipient's public RSA key. 118 | * @param {string} publicKey - The public key of the user that's going to receive the file. 119 | * @returns {void} 120 | */ 121 | async encryptChunks(publicKey) { 122 | this.encryption = true; 123 | this.key = CryptoFD.generateAESKey(); 124 | this.encryptedKey = await CryptoFD.encryptRSA(this.key, publicKey); 125 | } 126 | 127 | /** 128 | * Stops reading from the file and cancels the operation. 129 | * @returns {void} 130 | */ 131 | destroy() { 132 | this.stop = true; 133 | this.reader = null; 134 | } 135 | } -------------------------------------------------------------------------------- /src/assets/js/CryptoFD.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A class with static methods to simplify the use of cryptographic functions throughout the application. 3 | */ 4 | class CryptoFD { 5 | /** 6 | * @param {ArrayBuffer} data - The file data as an ArrayBuffer. 7 | * @returns {string} - An encoded string version of the file data. 8 | */ 9 | static encode(data) { 10 | return String.fromCharCode.apply(null, new Uint8Array(data)); 11 | } 12 | 13 | /** 14 | * @param {string} plaintext - The string to encrypt. 15 | * @param {string} password - The encryption password. 16 | * @returns {string} - The ciphertext. 17 | */ 18 | static encryptAES(plaintext, password) { 19 | let encrypted = CryptoJS.AES.encrypt(plaintext, password, { 20 | mode: CryptoJS.mode.CFB, 21 | padding: CryptoJS.pad.Pkcs7 22 | }); 23 | 24 | return encrypted.toString(); 25 | } 26 | 27 | /** 28 | * @param {string} ciphertext - The string to decrypt. 29 | * @param {string} password - The decryption password. 30 | * @returns {string} - The plaintext. 31 | */ 32 | static decryptAES(ciphertext, password) { 33 | let decrypted = CryptoJS.AES.decrypt(ciphertext, password, { 34 | mode: CryptoJS.mode.CFB, 35 | padding: CryptoJS.pad.Pkcs7 36 | }); 37 | 38 | return decrypted.toString(CryptoJS.enc.Utf8); 39 | } 40 | 41 | /** 42 | * @param {string} plaintext - The string to encrypt. 43 | * @param {string} publicKey - The public RSA key to encrypt the data with. 44 | * @returns {string} - A Base64 encoded version of the ciphertext. 45 | */ 46 | static encryptRSA(plaintext, publicKey) { 47 | return new Promise((resolve) => { 48 | publicKey = forge.pki.publicKeyFromPem(publicKey); 49 | resolve(btoa(publicKey.encrypt(plaintext, "RSA-OAEP"))); 50 | }); 51 | } 52 | 53 | /** 54 | * @param {string} ciphertext - The string to decrypt. 55 | * @param {string} privateKey - The private RSA key to decrypt the data with. 56 | * @returns {string} - The plaintext. 57 | */ 58 | static decryptRSA(ciphertext, privateKey) { 59 | return new Promise((resolve) => { 60 | privateKey = forge.pki.privateKeyFromPem(privateKey); 61 | resolve(privateKey.decrypt(atob(ciphertext), "RSA-OAEP")); 62 | }); 63 | } 64 | 65 | /** 66 | * Generates a random password, and a cryptographically random salt, from which a 256-bit AES key is derived. 67 | * @returns {string} - The AES key. 68 | */ 69 | static generateAESKey() { 70 | let result = ""; 71 | let characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; 72 | let charactersLength = characters.length; 73 | 74 | for(let i = 0; i < charactersLength; i++) { 75 | result += characters.charAt(Math.floor(Math.random() * charactersLength)); 76 | } 77 | 78 | let salt = CryptoJS.lib.WordArray.random(128/8); 79 | 80 | return CryptoJS.PBKDF2(result, salt, { keySize: 256/32 }).toString(CryptoJS.enc.Base64); 81 | } 82 | 83 | /** 84 | * Generate a public and private RSA key pair. 85 | * @returns {Object} - An object containing both the public and private key. 86 | */ 87 | static generateRSAKeys() { 88 | let rsa = forge.pki.rsa; 89 | 90 | return new Promise((resolve, reject) => { 91 | rsa.generateKeyPair({ bits:2048, workers:-1 }, (error, keys) => { 92 | if(error) { 93 | reject(error); 94 | } else { 95 | resolve({ publicKey:forge.pki.publicKeyToPem(keys.publicKey), privateKey:forge.pki.privateKeyToPem(keys.privateKey) }); 96 | } 97 | }); 98 | }); 99 | } 100 | } -------------------------------------------------------------------------------- /src/assets/js/FileSaver.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A class that simplifies the process of turning data into a Blob and saving it. 3 | */ 4 | class FileSaver { 5 | /** 6 | * @param {string} privateKey - The private RSA key of the user receiving the file. 7 | * @param {string} fileName - The name of the file. 8 | * @param {string} fileSize - The size of the file. 9 | * @returns {void} 10 | * @constructor 11 | */ 12 | constructor(privateKey, fileName, fileSize) { 13 | this.privateKey = privateKey; 14 | this.fileName = fileName; 15 | this.fileSize = fileSize; 16 | this.fileData = []; 17 | this.key = ""; 18 | } 19 | 20 | /** 21 | * Appends each file chunk to an array. If the chunk data is encrypted, then the AES key is decrypted using the recipient's private RSA key. The decrypted AES key is then used to decrypt the chunk data before appending it to the array. 22 | * @param {Object} data - An object containing the chunk data from a file, the chunk number, and (optionally) the decryption key. 23 | * @returns {void} 24 | */ 25 | async append(data) { 26 | if("key" in data) { 27 | if(empty(this.key) || data.chunk === 1) { 28 | this.key = await CryptoFD.decryptRSA(data.key, this.privateKey); 29 | } 30 | 31 | let buffer = this.decryptChunk(data.chunkData, this.key); 32 | 33 | this.fileData.push(buffer); 34 | } else { 35 | this.fileData.push(data.chunkData); 36 | } 37 | } 38 | 39 | /** 40 | * Decrypts and turns the encoded chunk data into an ArrayBuffer. 41 | * @param {string} chunkData - The encoded chunk data. 42 | * @param {string} key - The AES decryption key. 43 | * @returns {ArrayBuffer} - An ArrayBuffer containing the chunk data. 44 | */ 45 | decryptChunk(chunkData, key) { 46 | let decrypted = CryptoFD.decryptAES(chunkData, key); 47 | 48 | let buffer = new ArrayBuffer(decrypted.length); 49 | let bufferView = new Uint8Array(buffer); 50 | for(let i = 0, length = decrypted.length; i < length; i++) { 51 | bufferView[i] = decrypted.charCodeAt(i); 52 | } 53 | 54 | return buffer; 55 | } 56 | 57 | /** 58 | * Creates an HTML anchor element with the file data as its "href" attribute value. 59 | * @param {string} url - The URL to be used as the "href" attribute of the anchor element. In this case, the file data as a DOMString. 60 | * @param {string} filename - The name of the file. 61 | * @returns {HTMLAnchorElement} - The anchor element. 62 | */ 63 | createLink(url, filename) { 64 | let link = document.createElement("a"); 65 | 66 | link.style = "display:none"; 67 | link.href = url; 68 | link.download = filename; 69 | 70 | return link; 71 | } 72 | 73 | /** 74 | * Turns the file data into a Blob, which is then turned into a DOMString and set as the "href" attribute value of an HTML anchor element. The link is then appended to the body before being clicked on and removed. This causes the file to be downloaded. 75 | * @returns {void} 76 | */ 77 | save() { 78 | let blob = new Blob(this.fileData); 79 | 80 | let url = window.URL.createObjectURL(blob); 81 | let link = this.createLink(url, this.fileName); 82 | 83 | document.body.appendChild(link); 84 | 85 | link.click(); 86 | 87 | document.body.removeChild(link); 88 | 89 | setTimeout(function() { 90 | window.URL.revokeObjectURL(url); 91 | }, 1000); 92 | } 93 | } -------------------------------------------------------------------------------- /src/assets/js/Uploader.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A class to simplify the upload procedure. 3 | */ 4 | class Uploader { 5 | /** 6 | * @param {Socket} socket - The client's socket. 7 | * @param {string} from - The client sending the file. 8 | * @param {string} to - The client receiving the file. 9 | * @param {Object} file - The file being sent. 10 | * @param {Boolean} encryption - Whether or not the data is encrypted. 11 | * @returns {void} 12 | * @constructor 13 | */ 14 | constructor(socket, from, to, file, encryption) { 15 | this.part = 0; 16 | this.socket = socket; 17 | this.from = from; 18 | this.to = to; 19 | this.file = file; 20 | this.encryption = encryption; 21 | this.stop = false; 22 | } 23 | 24 | /** 25 | * Sends an object containing the chunk data and other required details to the server, which then relays it to the recipient of the file. 26 | * @param {Object} data - The data to send to the recipient. 27 | * @returns {void} 28 | */ 29 | upload(data) { 30 | if(!this.stop) { 31 | this.part++; 32 | 33 | if(this.part === 1) { 34 | data["filesize"] = this.file.size; 35 | data["filename"] = this.file.name; 36 | } 37 | 38 | data["from"] = this.from; 39 | data["to"] = this.to; 40 | 41 | this.socket.emit("upload", data); 42 | } 43 | } 44 | 45 | /** 46 | * Used to inform the other client that the upload has been completed, and whether or not it was cancelled. 47 | * @param {Boolean} cancelled - Whether or not the upload was cancelled. 48 | * @returns {void} 49 | */ 50 | finish(cancelled) { 51 | this.socket.emit("uploaded", { from:this.from, to:this.to, encryption:this.encryption, filename:this.file.name, cancelled:cancelled }); 52 | } 53 | 54 | /** 55 | * Stop and cancel the upload. 56 | * @returns {void} 57 | */ 58 | destroy() { 59 | this.socket = null; 60 | this.stop = true; 61 | } 62 | } -------------------------------------------------------------------------------- /src/assets/js/forge.all.min.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"forge.min.js","sources":["webpack:///forge.min.js"],"mappings":"AAAA;AAm+HA;AAuvJA;AAm2BA;AAkvFA;AAo4GA;AAq4FA;AAwtHA;AAo6FA","sourceRoot":""} -------------------------------------------------------------------------------- /src/assets/js/notifier.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Website: https://www.xtrendence.com 3 | * Portfolio: https://www.xtrendence.dev 4 | * GitHub: https://www.github.com/Xtrendence 5 | */ 6 | class Notifier{constructor(t){this.position=this.empty(t)?"TopRight":t,this.defaults={width:"250px",borderRadius:"10px",duration:5e3,color:"rgb(255,255,255)",success:{title:"Success Notification",description:"Whatever you did, it worked.",background:"rgb(40,200,80)"},error:{title:"Error Notification",description:"That didn't work out, did it?",background:"rgb(230,50,50)"},alert:{title:"Alert Notification",description:"This is probably important...",background:"rgb(240,180,10)"},info:{title:"Info Notification",description:"Just so you know...",background:"rgb(170,80,220)"},html:""};let e=document.getElementsByTagName("head")[0],i=document.createElement("style");i.id="x-notify-style",i.innerHTML="::-webkit-scrollbar { display:none; }",e.appendChild(i)}setOptions(t,e){this.width=this.empty(t.width)?this.defaults.width:t.width,this.borderRadius=this.empty(t.borderRadius)?this.defaults.borderRadius:t.borderRadius,this.title=this.empty(t.title)?this.defaults[e].title:t.title,this.description=this.empty(t.description)?this.defaults[e].description:t.description,this.duration=this.empty(t.duration)?this.defaults.duration:t.duration,this.background=this.empty(t.background)?this.defaults[e].background:t.background,this.color=this.empty(t.color)?this.defaults.color:t.color,this.html=this.empty(t.html)?this.defaults.html:t.html}success(t){this.setOptions(t,"success");let e=this.createElement();this.showNotification(e)}error(t){this.setOptions(t,"error");let e=this.createElement();this.showNotification(e)}alert(t){this.setOptions(t,"alert");let e=this.createElement();this.showNotification(e)}info(t){this.setOptions(t,"info");let e=this.createElement();this.showNotification(e)}createElement(){if(!document.getElementById("x-notify-container")){let t=document.getElementsByTagName("body")[0],e="calc(100% - 20px)",i="20px",o="0",s="0",n="0",r="auto",l="auto";switch(this.position){case"BottomRight":e="auto",s="auto",r="0";break;case"BottomLeft":e="auto",i="0",o="20px",s="auto",n="auto",r="0",l="0";break;case"TopLeft":i="0",o="20px",n="auto",l="0";break}let a=document.createElement("div");a.id="x-notify-container",a.style="position:fixed; z-index:1000; width:calc("+this.width+" + 70px); height:"+e+"; pointer-events:none; overflow-x:hidden; overflow-y:auto; -webkit-overflow-scrolling:touch; scroll-behavior:smooth; scrollbar-width:none; padding-top:20px; padding-right:"+i+"; padding-left:"+o+"; top:"+s+"; right:"+n+"; bottom:"+r+"; left:"+l+";",t.appendChild(a)}let t="TopRight"===this.position||"BottomRight"===this.position?"right":"left",e=document.createElement("div");e.id=this.generateID(),e.style="display:block; padding:0 0 20px 0; text-align:"+t+"; width:100%;";let i=document.createElement("div");return i.classList.add("x-notification"),i.style="backdrop-filter:blur(2px); background:"+this.background+"; color:"+this.color+"; width:"+this.width+"; border-radius:"+this.borderRadius+'; padding:10px 12px 12px 12px; font-family:"Helvetica Neue", "Lucida Grande", "Arial", "Verdana", "Tahoma", sans-serif; display:inline-block; text-align:left; opacity:0; pointer-events:auto; -webkit-user-select:none; -khtml-user-select:none; -moz-user-select:none; -ms-user-select:none; user-select:none; outline:none;',i.innerHTML=''+this.title+''+this.description+""+this.html,e.append(i),e}showNotification(t){let e=document.getElementById("x-notify-container"),i=t.getElementsByClassName("x-notification")[0];"BottomRight"===this.position||"BottomLeft"===this.position?(e.append(t),e.scrollHeight>window.innerHeight&&(e.style.height="calc(100% - 20px)"),e.scrollTo(0,e.scrollHeight)):e.prepend(t);let o=.05,s=setInterval((()=>{o+=.05,i.style.opacity=o,o>=1&&(i.style.opacity=1,clearInterval(s))}),10);setTimeout((()=>{this.hideNotification(t)}),this.duration)}hideNotification(t){let e=document.getElementById("x-notify-container"),i=t.getElementsByClassName("x-notification")[0],o=1,s=setInterval((()=>{o-=.05,i.style.opacity=o,o<=0&&(t.remove(),!this.empty(e)&&this.empty(e.innerHTML)&&e.remove(),clearInterval(s))}),10);!this.empty(e)&&e.scrollHeight<=window.innerHeight&&(e.style.height="auto")}clear(){let t=document.getElementById("x-notify-container").getElementsByClassName("x-notification");for(let e=0;e0;){let i=parseInt(Math.random()*t),o=e[--t];e[t]=e[i],e[i]=o}return e.join("")}epoch(){var t=new Date;return Math.round(t.getTime()/1e3)}empty(t){return null==t||""===t.toString().trim()}} -------------------------------------------------------------------------------- /src/assets/js/portableMain.js: -------------------------------------------------------------------------------- 1 | document.addEventListener("DOMContentLoaded", () => { 2 | const electron = require("electron"); 3 | const { ipcRenderer } = electron; 4 | 5 | let buttonClose = document.getElementsByClassName("close-button")[0]; 6 | let buttonMinimize = document.getElementsByClassName("minimize-button")[0]; 7 | 8 | let spanAddress = document.getElementById("server-address"); 9 | let buttonOpen = document.getElementById("open-button"); 10 | let buttonStop = document.getElementById("stop-button"); 11 | 12 | // Open the application in the browser. 13 | buttonOpen.addEventListener("click", () => { 14 | ipcRenderer.send("open-link", spanAddress.textContent); 15 | }); 16 | 17 | // Quit the application. 18 | buttonStop.addEventListener("click", () => { 19 | ipcRenderer.send("quit"); 20 | }); 21 | 22 | // If on macOS, hide the application. Otherwise, quit it. 23 | buttonClose.addEventListener("click", () => { 24 | setWindowState("closed"); 25 | }); 26 | 27 | // Minimize the application. 28 | buttonMinimize.addEventListener("click", () => { 29 | setWindowState("minimized"); 30 | }); 31 | 32 | /** 33 | * Set the state of the application window. 34 | * @param {string} state - The state of the application window. 35 | * @returns {void} 36 | */ 37 | function setWindowState(state) { 38 | ipcRenderer.send("set-window-state", state); 39 | } 40 | }); -------------------------------------------------------------------------------- /src/assets/js/prime.worker.min.js: -------------------------------------------------------------------------------- 1 | !function(t){var i={};function r(o){if(i[o])return i[o].exports;var s=i[o]={i:o,l:!1,exports:{}};return t[o].call(s.exports,s,s.exports,r),s.l=!0,s.exports}r.m=t,r.c=i,r.d=function(t,i,o){r.o(t,i)||Object.defineProperty(t,i,{enumerable:!0,get:o})},r.r=function(t){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})},r.t=function(t,i){if(1&i&&(t=r(t)),8&i)return t;if(4&i&&"object"==typeof t&&t&&t.__esModule)return t;var o=Object.create(null);if(r.r(o),Object.defineProperty(o,"default",{enumerable:!0,value:t}),2&i&&"string"!=typeof t)for(var s in t)r.d(o,s,function(i){return t[i]}.bind(null,s));return o},r.n=function(t){var i=t&&t.__esModule?function(){return t.default}:function(){return t};return r.d(i,"a",i),i},r.o=function(t,i){return Object.prototype.hasOwnProperty.call(t,i)},r.p="",r(r.s=1)}([function(t,i){t.exports={options:{usePureJavaScript:!1}}},function(t,i,r){r(2),t.exports=r(0)},function(t,i,r){var o=r(0);r(3);var s=[2,3,5,7,11,13,17,19,23,29,31,37,41,43,47,53,59,61,67,71,73,79,83,89,97,101,103,107,109,113,127,131,137,139,149,151,157,163,167,173,179,181,191,193,197,199,211,223,227,229,233,239,241,251,257,263,269,271,277,281,283,293,307,311,313,317,331,337,347,349,353,359,367,373,379,383,389,397,401,409,419,421,431,433,439,443,449,457,461,463,467,479,487,491,499,503,509,521,523,541,547,557,563,569,571,577,587,593,599,601,607,613,617,619,631,641,643,647,653,659,661,673,677,683,691,701,709,719,727,733,739,743,751,757,761,769,773,787,797,809,811,821,823,827,829,839,853,857,859,863,877,881,883,887,907,911,919,929,937,941,947,953,967,971,977,983,991,997],e=(1<<26)/s[s.length-1],a=o.jsbn.BigInteger;new a(null).fromInt(2),self.addEventListener("message",(function(t){var i=function(t){for(var i=new a(t.hex,16),r=0,o=t.workLoad,s=0;s=0);var u=o.modPow(s,t);if(0!==u.compareTo(a.ONE)&&0!==u.compareTo(i)){for(var f=r;--f;){if(0===(u=u.modPowInt(2,t)).compareTo(a.ONE))return!1;if(0===u.compareTo(i))break}if(0===f)return!1}}var p;return!0}(t)}},function(t,i,r){var o,s=r(0);t.exports=s.jsbn=s.jsbn||{};function e(t,i,r){this.data=[],null!=t&&("number"==typeof t?this.fromNumber(t,i,r):null==i&&"string"!=typeof t?this.fromString(t,256):this.fromString(t,i))}function a(){return new e(null)}function n(t,i,r,o,s,e){for(var a=16383&i,n=i>>14;--e>=0;){var h=16383&this.data[t],u=this.data[t++]>>14,f=n*h+u*a;s=((h=a*h+((16383&f)<<14)+r.data[o]+s)>>28)+(f>>14)+n*u,r.data[o++]=268435455&h}return s}s.jsbn.BigInteger=e,"undefined"==typeof navigator?(e.prototype.am=n,o=28):"Microsoft Internet Explorer"==navigator.appName?(e.prototype.am=function(t,i,r,o,s,e){for(var a=32767&i,n=i>>15;--e>=0;){var h=32767&this.data[t],u=this.data[t++]>>15,f=n*h+u*a;s=((h=a*h+((32767&f)<<15)+r.data[o]+(1073741823&s))>>>30)+(f>>>15)+n*u+(s>>>30),r.data[o++]=1073741823&h}return s},o=30):"Netscape"!=navigator.appName?(e.prototype.am=function(t,i,r,o,s,e){for(;--e>=0;){var a=i*this.data[t++]+r.data[o]+s;s=Math.floor(a/67108864),r.data[o++]=67108863&a}return s},o=26):(e.prototype.am=n,o=28),e.prototype.DB=o,e.prototype.DM=(1<>>16)&&(t=i,r+=16),0!=(i=t>>8)&&(t=i,r+=8),0!=(i=t>>4)&&(t=i,r+=4),0!=(i=t>>2)&&(t=i,r+=2),0!=(i=t>>1)&&(t=i,r+=1),r}function l(t){this.m=t}function v(t){this.m=t,this.mp=t.invDigit(),this.mpl=32767&this.mp,this.mph=this.mp>>15,this.um=(1<>=16,i+=16),0==(255&t)&&(t>>=8,i+=8),0==(15&t)&&(t>>=4,i+=4),0==(3&t)&&(t>>=2,i+=2),0==(1&t)&&++i,i}function B(t){for(var i=0;0!=t;)t&=t-1,++i;return i}function S(){}function M(t){return t}function w(t){this.r2=a(),this.q3=a(),e.ONE.dlShiftTo(2*t.t,this.r2),this.mu=this.r2.divide(t),this.m=t}l.prototype.convert=function(t){return t.s<0||t.compareTo(this.m)>=0?t.mod(this.m):t},l.prototype.revert=function(t){return t},l.prototype.reduce=function(t){t.divRemTo(this.m,null,t)},l.prototype.mulTo=function(t,i,r){t.multiplyTo(i,r),this.reduce(r)},l.prototype.sqrTo=function(t,i){t.squareTo(i),this.reduce(i)},v.prototype.convert=function(t){var i=a();return t.abs().dlShiftTo(this.m.t,i),i.divRemTo(this.m,null,i),t.s<0&&i.compareTo(e.ZERO)>0&&this.m.subTo(i,i),i},v.prototype.revert=function(t){var i=a();return t.copyTo(i),this.reduce(i),i},v.prototype.reduce=function(t){for(;t.t<=this.mt2;)t.data[t.t++]=0;for(var i=0;i>15)*this.mpl&this.um)<<15)&t.DM;for(r=i+this.m.t,t.data[r]+=this.m.am(0,o,t,i,0,this.m.t);t.data[r]>=t.DV;)t.data[r]-=t.DV,t.data[++r]++}t.clamp(),t.drShiftTo(this.m.t,t),t.compareTo(this.m)>=0&&t.subTo(this.m,t)},v.prototype.mulTo=function(t,i,r){t.multiplyTo(i,r),this.reduce(r)},v.prototype.sqrTo=function(t,i){t.squareTo(i),this.reduce(i)},e.prototype.copyTo=function(t){for(var i=this.t-1;i>=0;--i)t.data[i]=this.data[i];t.t=this.t,t.s=this.s},e.prototype.fromInt=function(t){this.t=1,this.s=t<0?-1:0,t>0?this.data[0]=t:t<-1?this.data[0]=t+this.DV:this.t=0},e.prototype.fromString=function(t,i){var r;if(16==i)r=4;else if(8==i)r=3;else if(256==i)r=8;else if(2==i)r=1;else if(32==i)r=5;else{if(4!=i)return void this.fromRadix(t,i);r=2}this.t=0,this.s=0;for(var o=t.length,s=!1,a=0;--o>=0;){var n=8==r?255&t[o]:d(t,o);n<0?"-"==t.charAt(o)&&(s=!0):(s=!1,0==a?this.data[this.t++]=n:a+r>this.DB?(this.data[this.t-1]|=(n&(1<>this.DB-a):this.data[this.t-1]|=n<=this.DB&&(a-=this.DB))}8==r&&0!=(128&t[0])&&(this.s=-1,a>0&&(this.data[this.t-1]|=(1<0&&this.data[this.t-1]==t;)--this.t},e.prototype.dlShiftTo=function(t,i){var r;for(r=this.t-1;r>=0;--r)i.data[r+t]=this.data[r];for(r=t-1;r>=0;--r)i.data[r]=0;i.t=this.t+t,i.s=this.s},e.prototype.drShiftTo=function(t,i){for(var r=t;r=0;--r)i.data[r+a+1]=this.data[r]>>s|n,n=(this.data[r]&e)<=0;--r)i.data[r]=0;i.data[a]=n,i.t=this.t+a+1,i.s=this.s,i.clamp()},e.prototype.rShiftTo=function(t,i){i.s=this.s;var r=Math.floor(t/this.DB);if(r>=this.t)i.t=0;else{var o=t%this.DB,s=this.DB-o,e=(1<>o;for(var a=r+1;a>o;o>0&&(i.data[this.t-r-1]|=(this.s&e)<>=this.DB;if(t.t>=this.DB;o+=this.s}else{for(o+=this.s;r>=this.DB;o-=t.s}i.s=o<0?-1:0,o<-1?i.data[r++]=this.DV+o:o>0&&(i.data[r++]=o),i.t=r,i.clamp()},e.prototype.multiplyTo=function(t,i){var r=this.abs(),o=t.abs(),s=r.t;for(i.t=s+o.t;--s>=0;)i.data[s]=0;for(s=0;s=0;)t.data[r]=0;for(r=0;r=i.DV&&(t.data[r+i.t]-=i.DV,t.data[r+i.t+1]=1)}t.t>0&&(t.data[t.t-1]+=i.am(r,i.data[r],t,2*r,0,1)),t.s=0,t.clamp()},e.prototype.divRemTo=function(t,i,r){var o=t.abs();if(!(o.t<=0)){var s=this.abs();if(s.t0?(o.lShiftTo(f,n),s.lShiftTo(f,r)):(o.copyTo(n),s.copyTo(r));var p=n.t,d=n.data[p-1];if(0!=d){var c=d*(1<1?n.data[p-2]>>this.F2:0),l=this.FV/c,v=(1<=0&&(r.data[r.t++]=1,r.subTo(g,r)),e.ONE.dlShiftTo(p,g),g.subTo(n,n);n.t=0;){var D=r.data[--y]==d?this.DM:Math.floor(r.data[y]*l+(r.data[y-1]+T)*v);if((r.data[y]+=n.am(0,D,r,b,0,p))0&&r.rShiftTo(f,r),h<0&&e.ZERO.subTo(r,r)}}},e.prototype.invDigit=function(){if(this.t<1)return 0;var t=this.data[0];if(0==(1&t))return 0;var i=3&t;return(i=(i=(i=(i=i*(2-(15&t)*i)&15)*(2-(255&t)*i)&255)*(2-((65535&t)*i&65535))&65535)*(2-t*i%this.DV)%this.DV)>0?this.DV-i:-i},e.prototype.isEven=function(){return 0==(this.t>0?1&this.data[0]:this.s)},e.prototype.exp=function(t,i){if(t>4294967295||t<1)return e.ONE;var r=a(),o=a(),s=i.convert(this),n=m(t)-1;for(s.copyTo(r);--n>=0;)if(i.sqrTo(r,o),(t&1<0)i.mulTo(o,s,r);else{var h=r;r=o,o=h}return i.revert(r)},e.prototype.toString=function(t){if(this.s<0)return"-"+this.negate().toString(t);var i;if(16==t)i=4;else if(8==t)i=3;else if(2==t)i=1;else if(32==t)i=5;else{if(4!=t)return this.toRadix(t);i=2}var r,o=(1<0)for(n>n)>0&&(s=!0,e=p(r));a>=0;)n>(n+=this.DB-i)):(r=this.data[a]>>(n-=i)&o,n<=0&&(n+=this.DB,--a)),r>0&&(s=!0),s&&(e+=p(r));return s?e:"0"},e.prototype.negate=function(){var t=a();return e.ZERO.subTo(this,t),t},e.prototype.abs=function(){return this.s<0?this.negate():this},e.prototype.compareTo=function(t){var i=this.s-t.s;if(0!=i)return i;var r=this.t;if(0!=(i=r-t.t))return this.s<0?-i:i;for(;--r>=0;)if(0!=(i=this.data[r]-t.data[r]))return i;return 0},e.prototype.bitLength=function(){return this.t<=0?0:this.DB*(this.t-1)+m(this.data[this.t-1]^this.s&this.DM)},e.prototype.mod=function(t){var i=a();return this.abs().divRemTo(t,null,i),this.s<0&&i.compareTo(e.ZERO)>0&&t.subTo(i,i),i},e.prototype.modPowInt=function(t,i){var r;return r=t<256||i.isEven()?new l(i):new v(i),this.exp(t,r)},e.ZERO=c(0),e.ONE=c(1),S.prototype.convert=M,S.prototype.revert=M,S.prototype.mulTo=function(t,i,r){t.multiplyTo(i,r)},S.prototype.sqrTo=function(t,i){t.squareTo(i)},w.prototype.convert=function(t){if(t.s<0||t.t>2*this.m.t)return t.mod(this.m);if(t.compareTo(this.m)<0)return t;var i=a();return t.copyTo(i),this.reduce(i),i},w.prototype.revert=function(t){return t},w.prototype.reduce=function(t){for(t.drShiftTo(this.m.t-1,this.r2),t.t>this.m.t+1&&(t.t=this.m.t+1,t.clamp()),this.mu.multiplyUpperTo(this.r2,this.m.t+1,this.q3),this.m.multiplyLowerTo(this.q3,this.m.t+1,this.r2);t.compareTo(this.r2)<0;)t.dAddOffset(1,this.m.t+1);for(t.subTo(this.r2,t);t.compareTo(this.m)>=0;)t.subTo(this.m,t)},w.prototype.mulTo=function(t,i,r){t.multiplyTo(i,r),this.reduce(r)},w.prototype.sqrTo=function(t,i){t.squareTo(i),this.reduce(i)};var E=[2,3,5,7,11,13,17,19,23,29,31,37,41,43,47,53,59,61,67,71,73,79,83,89,97,101,103,107,109,113,127,131,137,139,149,151,157,163,167,173,179,181,191,193,197,199,211,223,227,229,233,239,241,251,257,263,269,271,277,281,283,293,307,311,313,317,331,337,347,349,353,359,367,373,379,383,389,397,401,409,419,421,431,433,439,443,449,457,461,463,467,479,487,491,499,503,509],O=(1<<26)/E[E.length-1];e.prototype.chunkSize=function(t){return Math.floor(Math.LN2*this.DB/Math.log(t))},e.prototype.toRadix=function(t){if(null==t&&(t=10),0==this.signum()||t<2||t>36)return"0";var i=this.chunkSize(t),r=Math.pow(t,i),o=c(r),s=a(),e=a(),n="";for(this.divRemTo(o,s,e);s.signum()>0;)n=(r+e.intValue()).toString(t).substr(1)+n,s.divRemTo(o,s,e);return e.intValue().toString(t)+n},e.prototype.fromRadix=function(t,i){this.fromInt(0),null==i&&(i=10);for(var r=this.chunkSize(i),o=Math.pow(i,r),s=!1,a=0,n=0,h=0;h=r&&(this.dMultiply(o),this.dAddOffset(n,0),a=0,n=0))}a>0&&(this.dMultiply(Math.pow(i,a)),this.dAddOffset(n,0)),s&&e.ZERO.subTo(this,this)},e.prototype.fromNumber=function(t,i,r){if("number"==typeof i)if(t<2)this.fromInt(1);else for(this.fromNumber(t,r),this.testBit(t-1)||this.bitwiseTo(e.ONE.shiftLeft(t-1),y,this),this.isEven()&&this.dAddOffset(1,0);!this.isProbablePrime(i);)this.dAddOffset(2,0),this.bitLength()>t&&this.subTo(e.ONE.shiftLeft(t-1),this);else{var o=new Array,s=7&t;o.length=1+(t>>3),i.nextBytes(o),s>0?o[0]&=(1<>=this.DB;if(t.t>=this.DB;o+=this.s}else{for(o+=this.s;r>=this.DB;o+=t.s}i.s=o<0?-1:0,o>0?i.data[r++]=o:o<-1&&(i.data[r++]=this.DV+o),i.t=r,i.clamp()},e.prototype.dMultiply=function(t){this.data[this.t]=this.am(0,t-1,this,0,0,this.t),++this.t,this.clamp()},e.prototype.dAddOffset=function(t,i){if(0!=t){for(;this.t<=i;)this.data[this.t++]=0;for(this.data[i]+=t;this.data[i]>=this.DV;)this.data[i]-=this.DV,++i>=this.t&&(this.data[this.t++]=0),++this.data[i]}},e.prototype.multiplyLowerTo=function(t,i,r){var o,s=Math.min(this.t+t.t,i);for(r.s=0,r.t=s;s>0;)r.data[--s]=0;for(o=r.t-this.t;s=0;)r.data[o]=0;for(o=Math.max(i-this.t,0);o0)if(0==i)r=this.data[0]%t;else for(var o=this.t-1;o>=0;--o)r=(i*r+this.data[o])%t;return r},e.prototype.millerRabin=function(t){var i=this.subtract(e.ONE),r=i.getLowestSetBit();if(r<=0)return!1;for(var o,s=i.shiftRight(r),a={nextBytes:function(t){for(var i=0;i=0);var h=o.modPow(s,this);if(0!=h.compareTo(e.ONE)&&0!=h.compareTo(i)){for(var u=1;u++>24},e.prototype.shortValue=function(){return 0==this.t?this.s:this.data[0]<<16>>16},e.prototype.signum=function(){return this.s<0?-1:this.t<=0||1==this.t&&this.data[0]<=0?0:1},e.prototype.toByteArray=function(){var t=this.t,i=new Array;i[0]=this.s;var r,o=this.DB-t*this.DB%8,s=0;if(t-- >0)for(o>o)!=(this.s&this.DM)>>o&&(i[s++]=r|this.s<=0;)o<8?(r=(this.data[t]&(1<>(o+=this.DB-8)):(r=this.data[t]>>(o-=8)&255,o<=0&&(o+=this.DB,--t)),0!=(128&r)&&(r|=-256),0==s&&(128&this.s)!=(128&r)&&++s,(s>0||r!=this.s)&&(i[s++]=r);return i},e.prototype.equals=function(t){return 0==this.compareTo(t)},e.prototype.min=function(t){return this.compareTo(t)<0?this:t},e.prototype.max=function(t){return this.compareTo(t)>0?this:t},e.prototype.and=function(t){var i=a();return this.bitwiseTo(t,T,i),i},e.prototype.or=function(t){var i=a();return this.bitwiseTo(t,y,i),i},e.prototype.xor=function(t){var i=a();return this.bitwiseTo(t,b,i),i},e.prototype.andNot=function(t){var i=a();return this.bitwiseTo(t,g,i),i},e.prototype.not=function(){for(var t=a(),i=0;i=this.t?0!=this.s:0!=(this.data[i]&1<1){var p=a();for(o.sqrTo(n[1],p);h<=f;)n[h]=a(),o.mulTo(p,n[h-2],n[h]),h+=2}var d,T,y=t.t-1,b=!0,g=a();for(s=m(t.data[y])-1;y>=0;){for(s>=u?d=t.data[y]>>s-u&f:(d=(t.data[y]&(1<0&&(d|=t.data[y-1]>>this.DB+s-u)),h=r;0==(1&d);)d>>=1,--h;if((s-=h)<0&&(s+=this.DB,--y),b)n[d].copyTo(e),b=!1;else{for(;h>1;)o.sqrTo(e,g),o.sqrTo(g,e),h-=2;h>0?o.sqrTo(e,g):(T=e,e=g,g=T),o.mulTo(g,n[d],e)}for(;y>=0&&0==(t.data[y]&1<=0?(r.subTo(o,r),i&&s.subTo(n,s),a.subTo(h,a)):(o.subTo(r,o),i&&n.subTo(s,n),h.subTo(a,h))}return 0!=o.compareTo(e.ONE)?e.ZERO:h.compareTo(t)>=0?h.subtract(t):h.signum()<0?(h.addTo(t,h),h.signum()<0?h.add(t):h):h},e.prototype.pow=function(t){return this.exp(t,new S)},e.prototype.gcd=function(t){var i=this.s<0?this.negate():this.clone(),r=t.s<0?t.negate():t.clone();if(i.compareTo(r)<0){var o=i;i=r,r=o}var s=i.getLowestSetBit(),e=r.getLowestSetBit();if(e<0)return i;for(s0&&(i.rShiftTo(e,i),r.rShiftTo(e,r));i.signum()>0;)(s=i.getLowestSetBit())>0&&i.rShiftTo(s,i),(s=r.getLowestSetBit())>0&&r.rShiftTo(s,r),i.compareTo(r)>=0?(i.subTo(r,i),i.rShiftTo(1,i)):(r.subTo(i,r),r.rShiftTo(1,r));return e>0&&r.lShiftTo(e,r),r},e.prototype.isProbablePrime=function(t){var i,r=this.abs();if(1==r.t&&r.data[0]<=E[E.length-1]){for(i=0;i { 29 | this.io.to("network").emit("ping"); 30 | }, pingInterval); 31 | } 32 | } 33 | 34 | /** 35 | * Adds event listeners to a Socket. 36 | * @param {Socket} socket - The socket to add event listeners to. 37 | * @returns {void} 38 | */ 39 | attach(socket) { 40 | let address = socket.handshake.address; 41 | 42 | // When a client is disconnected, they are removed from the server as a user. 43 | socket.on("disconnect", async () => { 44 | socket.leave("network"); 45 | 46 | if(address in this.clients) { 47 | this.clients[address].permissionManager.off("change"); 48 | } 49 | 50 | await this.removeClient(address); 51 | }); 52 | 53 | // Fetch the list of clients from the database, and broadcast it to all clients. 54 | socket.on("get-clients", async () => { 55 | let clients = await this.getClients(); 56 | socket.emit("client-list", clients); 57 | }); 58 | 59 | // Set the public RSA key of a client. 60 | socket.on("set-key", async key => { 61 | // Ensure the public key cannot be used as part of an XSS attack. 62 | if(utils.xssValid(key)) { 63 | this.clients[address]["key"] = key; 64 | 65 | await this.saveClients(); 66 | await this.broadcastList(); 67 | } else { 68 | socket.emit("notify", { 69 | title: "Invalid Key", 70 | description: "The provided key contains invalid characters.", 71 | duration: 4000, 72 | background: "rgb(230,20,20)", 73 | color: "rgb(255,255,255)" 74 | }); 75 | 76 | socket.emit("kick"); 77 | 78 | this.removeClient(address); 79 | } 80 | }); 81 | } 82 | 83 | /** 84 | * Fetch the list of clients from the database and broadcast it to every registered client. 85 | * @param {Boolean} changed - Whether or not clients have been added or removed. 86 | * @returns {void} 87 | */ 88 | async broadcastList(changed = false) { 89 | let clients = await this.getClients(); 90 | this.io.to("network").emit("client-list", clients); 91 | 92 | if(changed) { 93 | let list = []; 94 | Object.keys(clients).map(ip => { 95 | list.push(`${ip} => ${clients[ip]["username"]}`); 96 | }); 97 | 98 | console.log(utils.epoch(), ": \x1b[33m", list, "\x1b[0m"); 99 | } 100 | } 101 | 102 | /** 103 | * Fetch the list of clients from the database. 104 | * @returns {Object} - The list of clients. 105 | */ 106 | async getClients() { 107 | try { 108 | let clients = await this.db.fetch("clients"); 109 | return clients.data; 110 | } catch(error) { 111 | if(error.status !== 404) { 112 | console.log(error); 113 | } else { 114 | return {}; 115 | } 116 | } 117 | } 118 | 119 | /** 120 | * Check if a client exists. 121 | * @param {string} address - The IP address of the client. 122 | * @returns {Boolean} 123 | */ 124 | clientExists(address) { 125 | return Object.keys(this.clients).includes(address); 126 | } 127 | 128 | /** 129 | * Check if a username is taken. 130 | * @param {string} username - The username to check. 131 | * @returns {Boolean} 132 | */ 133 | usernameTaken(username) { 134 | let taken = false; 135 | 136 | Object.keys(this.clients).map(ip => { 137 | if(username.toLowerCase() === this.clients[ip]["username"].toLowerCase()) { 138 | taken = true; 139 | } 140 | }); 141 | 142 | return taken; 143 | } 144 | 145 | /** 146 | * Check if the maximum number of clients are connected. 147 | * @returns {Boolean} 148 | */ 149 | clientLimitReached() { 150 | return (Object.keys(this.clients).length >= this.clientLimit); 151 | } 152 | 153 | /** 154 | * Save the current list of clients to the database. 155 | * @returns {void} 156 | */ 157 | async saveClients() { 158 | let clients = DBAdapter.adapt(this.clients); 159 | 160 | try { 161 | await this.db.save("clients", clients, false); 162 | } catch(error) { 163 | await this.db.save("clients", clients, true); 164 | 165 | if(error.status !== 409) { 166 | console.log("saveClients()", error); 167 | } 168 | } 169 | } 170 | 171 | /** 172 | * Register a client as a user. 173 | * @param {Socket} socket - The client's Socket. 174 | * @param {string} username - The client's username. 175 | * @param {string} key - The client's public RSA key. 176 | * @returns {void} 177 | */ 178 | async addClient(socket, username, key) { 179 | let address = socket.handshake.address; 180 | 181 | // Ensure the client's public RSA key cannot be used as part of an XSS attack. 182 | if(utils.xssValid(key)) { 183 | // Ensure the maximum number of clients hasn't been reached. 184 | if(!this.clientLimitReached()) { 185 | // If the client already exists, remove them first. 186 | if(this.clientExists(address)) { 187 | await this.removeClient(address); 188 | } 189 | 190 | // Add the client to the "network" room. 191 | socket.join("network"); 192 | 193 | // Assign a color to the client. 194 | let color = utils.getColor(address, this.clients); 195 | 196 | // Add the "PermissionManager" event listeners to the client's socket. 197 | this.permissionManager.attach(socket); 198 | 199 | // Create a new "UploadManager" instance for the client, and add the relevant event listeners to their socket. 200 | let uploadManager = new UploadManager(this, this.permissionManager); 201 | uploadManager.attach(socket); 202 | 203 | // If the client's whitelist is modified, their "UploadManager" instance needs to be updated. 204 | this.permissionManager.on("change", (state) => { 205 | if(this.clientExists(address)) { 206 | this.clients[address]["permissionManager"] = state; 207 | uploadManager.updatePermissionManager(state); 208 | } 209 | }); 210 | 211 | // Add the client to the list of clients. 212 | this.clients[address] = { socket:socket, username:username, key:key, uploadManager:uploadManager, permissionManager:this.permissionManager, color:color.index }; 213 | 214 | // Add the remaining event listeners to the client's socket. 215 | this.attach(socket); 216 | 217 | // Set the client's background color. 218 | socket.emit("set-color", color.colors); 219 | 220 | // Inform the client that they're logged in. 221 | socket.emit("login", username); 222 | 223 | // Save the client list to the database, and broadcast the updated list to other registered users. 224 | await this.saveClients(); 225 | await this.broadcastList(true); 226 | } else { 227 | socket.emit("notify", { 228 | title: "Limit Reached", 229 | description: "Maximum number of devices are connected to the server.", 230 | duration: 30000, 231 | background: "rgb(230,20,20)", 232 | color: "rgb(255,255,255)" 233 | }); 234 | } 235 | } else { 236 | socket.emit("notify", { 237 | title: "Invalid Key", 238 | description: "The provided key contains invalid characters.", 239 | duration: 4000, 240 | background: "rgb(230,20,20)", 241 | color: "rgb(255,255,255)" 242 | }); 243 | 244 | socket.emit("kick"); 245 | 246 | this.removeClient(address); 247 | } 248 | } 249 | 250 | /** 251 | * Remove a client from the client list. 252 | * @param {string} address - The IP address of the client to remove. 253 | * @returns {void} 254 | */ 255 | async removeClient(address) { 256 | delete this.clients[address]; 257 | 258 | this.permissionManager.remove(address); 259 | 260 | await this.saveClients(); 261 | await this.broadcastList(true); 262 | } 263 | } -------------------------------------------------------------------------------- /src/modules/DBAdapter.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A class for converting the client list to one that can be stored by the database. 3 | */ 4 | module.exports = class DBAdapter { 5 | /** 6 | * Converts the client list to one that can be serialized. 7 | * @param {Object} clients - An object containing the clients connected to the server. 8 | * @returns {Object} - A converted/adapted object that can be stored in the database. 9 | */ 10 | static adapt(clients) { 11 | let adapted = {}; 12 | 13 | Object.keys(clients).map(ip => { 14 | adapted[ip] = { 15 | username: clients[ip]["username"], 16 | key: clients[ip]["key"] 17 | } 18 | }); 19 | 20 | return adapted; 21 | } 22 | } -------------------------------------------------------------------------------- /src/modules/DBManager.js: -------------------------------------------------------------------------------- 1 | const PouchDB = require("pouchdb"); 2 | 3 | /** 4 | * A class with methods that provide CRUD functionality. 5 | */ 6 | module.exports = class DBManager { 7 | /** 8 | * @param {string} db - The name of the database. 9 | * @returns {void} 10 | * @constructor 11 | */ 12 | constructor(db) { 13 | this.db = new PouchDB(db); 14 | } 15 | 16 | /** 17 | * Remove all clients from the database. 18 | * @returns {void} 19 | */ 20 | async clear() { 21 | this.remove("clients").then(() => { 22 | console.log("\x1b[34m", "Cleared Previous Clients", "\x1b[0m"); 23 | }).catch(error => { 24 | console.log(error); 25 | }); 26 | } 27 | 28 | /** 29 | * Saves a key and value pair to the database. 30 | * @param {string} key - The key associated with the value being stored. 31 | * @param {string} value - The data being stored. 32 | * @param {string} force - In the event of a conflict, whether or not to forcefully write to the database. 33 | * @returns {Promise} 34 | */ 35 | async save(key, value, force) { 36 | return new Promise(async (resolve, reject) => { 37 | this.exists(key).then(() => { 38 | this.db.get(key).then(document => { 39 | document.data = value; 40 | 41 | this.db.put(document, { force:force }).then(response => { 42 | resolve(response); 43 | }).catch(error => { 44 | if(error.status !== 409) { 45 | console.log("Exists - Save Error", error); 46 | } 47 | reject(error); 48 | }); 49 | }).catch(error => { 50 | console.log("Fetch Error", error); 51 | reject(error); 52 | }); 53 | }).catch(() => { 54 | this.db.put({ 55 | _id: key, 56 | data: value 57 | }).then(response => { 58 | resolve(response); 59 | }).catch(error => { 60 | if(error.status !== 409) { 61 | console.log("Create - Save Error", error); 62 | } 63 | reject(error); 64 | }); 65 | }); 66 | }); 67 | } 68 | 69 | /** 70 | * Removes data from the database based on a given key. 71 | * @param {string} key - The key to remove from the database. 72 | * @returns {Promise} 73 | */ 74 | remove(key) { 75 | return new Promise((resolve, reject) => { 76 | this.exists(key).then(() => { 77 | this.db.get(key).then(document => { 78 | this.db.remove(document).then(response => { 79 | resolve(response); 80 | }).catch(error => { 81 | console.log("Remove Error", error); 82 | reject(error); 83 | }); 84 | }).catch(error => { 85 | console.log("Fetch Error", error); 86 | reject(error); 87 | }); 88 | }).catch(() => { 89 | resolve(); 90 | }); 91 | }); 92 | } 93 | 94 | /** 95 | * Check if a key exists in the database. 96 | * @param {string} key - The key to check the database for. 97 | * @returns {Promise} 98 | */ 99 | exists(key) { 100 | return new Promise((resolve, reject) => { 101 | this.db.get(key).then(() => { 102 | resolve(true); 103 | }).catch(() => { 104 | reject(false); 105 | }); 106 | }); 107 | } 108 | 109 | /** 110 | * Fetch the value of a key. 111 | * @param {string} key - The key to fetch the value of. 112 | * @returns {Promise} 113 | */ 114 | fetch(key) { 115 | return new Promise((resolve, reject) => { 116 | this.db.get(key).then(response => { 117 | resolve(response); 118 | }).catch(error => { 119 | if(error.status !== 404) { 120 | console.log(error); 121 | } 122 | reject(error); 123 | }); 124 | }); 125 | } 126 | } -------------------------------------------------------------------------------- /src/modules/PermissionManager.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A class to manage client permissions. 3 | */ 4 | module.exports = class PermissionManager { 5 | /** 6 | * @param {ConnectionManager} connectionManager - The "ConnectionManager" instance. 7 | * @returns {void} 8 | * @constructor 9 | */ 10 | constructor(connectionManager) { 11 | this.connectionManager = connectionManager; 12 | this.whitelist = {}; 13 | this.events = {}; 14 | } 15 | 16 | /** 17 | * Checks if an event exists. 18 | * @param {string} event - The name of the event. 19 | * @returns {Boolean} 20 | */ 21 | hasEvent(event) { 22 | return Object.keys(this.events).includes(event); 23 | } 24 | 25 | /** 26 | * Adds an event listener. 27 | * @param {string} event - The name of the event. 28 | * @returns {void} 29 | */ 30 | on(event, callback) { 31 | this.events[event] = callback; 32 | } 33 | 34 | /** 35 | * Removes an event listener. 36 | * @param {string} event - The name of the event. 37 | * @returns {void} 38 | */ 39 | off(event) { 40 | delete this.events[event]; 41 | } 42 | 43 | /** 44 | * Remove an IP address from a client's whitelist. 45 | * @param {string} address - The IP address to remove. 46 | * @returns {void} 47 | */ 48 | remove(address) { 49 | delete this.whitelist[address]; 50 | } 51 | 52 | /** 53 | * Check if a client's whitelist contains an IP address. 54 | * @param {string} address - The client whose whitelist should be checked. 55 | * @param {string} client - The IP address to check the whitelist for. 56 | * @returns {Boolean} 57 | */ 58 | whitelistContains(address, client) { 59 | if(!(address in this.whitelist)) { 60 | return false; 61 | } 62 | 63 | if(!Object.keys(this.whitelist[address]).includes(client)) { 64 | return false; 65 | } 66 | 67 | return !(this.whitelist[address][client]["allowed"] === true); 68 | } 69 | 70 | /** 71 | * Adds event listeners to a Socket. 72 | * @param {Socket} socket - The socket to add event listeners to. 73 | * @returns {void} 74 | */ 75 | attach(socket) { 76 | let address = socket.handshake.address; 77 | 78 | // Used to relay a permission request from one client to another. 79 | socket.on("ask-permission", data => { 80 | // If the recipient has already whitelisted or blocked the client asking for permission, no data is sent to them. 81 | if(!this.whitelistContains(data.to, data.from)) { 82 | let clients = this.connectionManager.clients; 83 | 84 | if(data.to in clients) { 85 | try { 86 | if(this.whitelist[data.to][data.from]["allowed"] === true) { 87 | socket.emit("update-permission", { from:data.to, response:true }); 88 | } else { 89 | this.connectionManager.io.to(clients[data.to].socket.id).emit("ask-permission", data.from); 90 | } 91 | } catch(error) { 92 | this.connectionManager.io.to(clients[data.to].socket.id).emit("ask-permission", data.from); 93 | } 94 | } 95 | } else { 96 | socket.emit("update-permission", { from:data.to, response:false }); 97 | 98 | socket.emit("notify", { 99 | title: "Requests Blocked", 100 | description: "This client has blocked your requests.", 101 | duration: 4000, 102 | background: "rgb(230,20,20)", 103 | color: "rgb(255,255,255)" 104 | }); 105 | } 106 | }); 107 | 108 | // Used to whitelist or block a client. 109 | socket.on("update-permission", data => { 110 | this.whitelist[address] = data.whitelist; 111 | 112 | let clients = this.connectionManager.clients; 113 | if(data.to in clients) { 114 | this.connectionManager.io.to(clients[data.to].socket.id).emit("update-permission", { from:address, response:data.response }); 115 | } 116 | 117 | if(this.hasEvent("change")) { 118 | this.events["change"](this); 119 | } 120 | }); 121 | } 122 | } -------------------------------------------------------------------------------- /src/modules/UploadManager.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A class to manage client uploads. 3 | */ 4 | module.exports = class UploadManager { 5 | /** 6 | * @param {ConnectionManager} connectionManager - The "ConnectionManager" instance. 7 | * @param {PermissionManager} permissionManager - The client's "PermissionManager" instance. 8 | * @returns {void} 9 | * @constructor 10 | */ 11 | constructor(connectionManager, permissionManager) { 12 | this.connectionManager = connectionManager; 13 | this.permissionManager = permissionManager; 14 | } 15 | 16 | /** 17 | * Update the "PermissionManager" instance, which contains the client's whitelist. 18 | * @param {PermissionManager} permissionManager - The updated "PermissionManager" instance. 19 | * @returns {void} 20 | */ 21 | updatePermissionManager(permissionManager) { 22 | this.permissionManager = permissionManager; 23 | } 24 | 25 | /** 26 | * Get a client's whitelist. 27 | * @param {string} address - The IP address of the client. 28 | * @returns {Array} - An array of IP addresses whitelisted by the client. 29 | */ 30 | getWhitelist(address) { 31 | let allowed = []; 32 | let whitelist = this.permissionManager.whitelist; 33 | if(address in whitelist) { 34 | Object.keys(whitelist[address]).map(client => { 35 | if(whitelist[address][client]["allowed"] === true) { 36 | allowed.push(client); 37 | } 38 | }); 39 | } 40 | return allowed; 41 | } 42 | 43 | /** 44 | * Adds event listeners to a Socket. 45 | * @param {Socket} socket - The socket to add event listeners to. 46 | * @returns {void} 47 | */ 48 | attach(socket) { 49 | // Used to inform a client that an upload was finished. 50 | socket.on("uploaded", data => { 51 | let whitelist = this.getWhitelist(data.to); 52 | let clients = this.connectionManager.clients; 53 | 54 | if(whitelist.includes(data.from)) { 55 | this.connectionManager.io.to(clients[data.to].socket.id).emit("uploaded", { from:data.from, encryption:data.encryption, filename:data.filename, cancelled:data.cancelled }); 56 | } 57 | }); 58 | 59 | // Used to relay chunk data to the recipient of a file. 60 | socket.on("upload", async data => { 61 | let whitelist = this.getWhitelist(data.to); 62 | let clients = this.connectionManager.clients; 63 | 64 | if(whitelist.includes(data.from)) { 65 | this.connectionManager.io.to(clients[data.to].socket.id).emit("upload", data); 66 | } 67 | }); 68 | } 69 | } -------------------------------------------------------------------------------- /src/modules/Utils.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | /** 3 | * Gets the local IP address of the server. 4 | * @returns {string} - The server's IP address. 5 | */ 6 | getIP() { 7 | const { networkInterfaces } = require("os"); 8 | 9 | const interfaces = networkInterfaces(); 10 | const ips = {}; 11 | 12 | for(const networkInterface of Object.keys(interfaces)) { 13 | for(const net of interfaces[networkInterface]) { 14 | if(net.family === "IPv4" && !net.internal && !networkInterface.toLowerCase().match("(vethernet|vmware|vm|area)")) { 15 | if(!ips[networkInterface]) { 16 | ips[networkInterface] = []; 17 | } 18 | ips[networkInterface].push(net.address); 19 | } 20 | } 21 | } 22 | 23 | const ip = ips[Object.keys(ips)[0]][0]; 24 | 25 | return ip; 26 | }, 27 | 28 | /** 29 | * Gets the IP address of a client based on the X-Forwarded-For header value. If the header is not found or is empty, a random IP is assigned to the client. 30 | * @param {Object} clients - The list of clients connected to the server. 31 | * @param {Socket} socket - The socket to get the IP address from. 32 | * @returns {string} - The IP address of the client. 33 | */ 34 | getClientIP(clients, socket) { 35 | try { 36 | let ip; 37 | 38 | if("handshake" in socket && "headers" in socket.handshake && !this.empty(socket.handshake.headers["x-forwarded-for"])) { 39 | ip = socket.handshake.headers["x-forwarded-for"]; 40 | } 41 | 42 | if("request" in socket && "headers" in socket.request && !this.empty(socket.request.headers["x-forwarded-for"])) { 43 | ip = socket.request.headers["x-forwarded-for"]; 44 | } 45 | 46 | if("handshake" in socket && "headers" in socket.handshake && !this.empty(socket.handshake.headers["X-Forwarded-For"])) { 47 | ip = socket.handshake.headers["X-Forwarded-For"]; 48 | } 49 | 50 | if("request" in socket && "headers" in socket.request && !this.empty(socket.request.headers["X-Forwarded-For"])) { 51 | ip = socket.request.headers["X-Forwarded-For"]; 52 | } 53 | 54 | if(this.empty(ip) || (ip in clients)) { 55 | ip = this.randomIP(clients); 56 | } 57 | 58 | return ip; 59 | } catch(error) { 60 | console.log(error); 61 | } 62 | }, 63 | 64 | /** 65 | * If an IPv4 address is in the format of an IPv6 one, it's converted to the standard IPv4 format. 66 | * @param {string} ip - The IP address to convert. 67 | * @returns {string} - The converted IPv4 address. 68 | */ 69 | IPv4(ip) { 70 | return ip.replace("::ffff:", ""); 71 | }, 72 | 73 | /** 74 | * Check if a Docker environment file exists. 75 | * @returns {Boolean} 76 | */ 77 | hasDockerEnvironment() { 78 | try { 79 | require("fs").statSync("/.dockerenv"); 80 | return true; 81 | } catch { 82 | return false; 83 | } 84 | }, 85 | 86 | /** 87 | * Check if the control group file contains the word "docker". 88 | * @returns {Boolean} 89 | */ 90 | hasDockerGroup() { 91 | try { 92 | return require("fs").readFileSync("/proc/self/cgroup", "utf8").includes("docker"); 93 | } catch { 94 | return false; 95 | } 96 | }, 97 | 98 | /** 99 | * Check if the arguments passed to the script contain "testing" as an argument. 100 | * @param {Array} args - An array of arguments. 101 | * @returns {Boolean} 102 | */ 103 | testingMode(args) { 104 | if(!this.empty(args) && args[0] === "testing") { 105 | return true; 106 | } 107 | return false; 108 | }, 109 | 110 | /** 111 | * Check if a file called "portable" exists in the root directory of the application, or if "portable" is an argument passed to the script. 112 | * @param {Array} args - An array of arguments. 113 | * @returns {Boolean} 114 | */ 115 | portableMode(args) { 116 | const path = require("path"); 117 | const fs = require("fs"); 118 | 119 | let portableFile = path.join(__dirname, "../portable"); 120 | 121 | if((!this.empty(args) && args[0] === "portable") || fs.existsSync(portableFile)) { 122 | return true; 123 | } 124 | 125 | return false; 126 | }, 127 | 128 | /** 129 | * Get the path to the user data directory. 130 | * @returns {string} - The path to the user data directory. 131 | */ 132 | getUserDirectory() { 133 | const { app } = require("electron"); 134 | return app.getPath("userData"); 135 | }, 136 | 137 | /** 138 | * Generates and returns a random and available IPv4 address. 139 | * @param {Object} clients - The list of connected clients. 140 | * @returns {string} - The randomly generated IP address. 141 | */ 142 | randomIP(clients) { 143 | let ips = Object.keys(clients); 144 | let ip = "192.168.1." + this.randomBetween(64, 256); 145 | 146 | while(ip in ips) { 147 | ip = "192.168.1." + this.randomBetween(64, 256); 148 | } 149 | 150 | return ip; 151 | }, 152 | 153 | /** 154 | * Checks if a username is valid. 155 | * @param {string} username - The username to check. 156 | * @returns {Boolean} 157 | */ 158 | validUsername(username) { 159 | try { 160 | if(username.length > 16) { 161 | return false; 162 | } 163 | 164 | return (/^[A-Za-z0-9]+$/.test(username)); 165 | } catch(error) { 166 | console.log(error); 167 | return false; 168 | } 169 | }, 170 | 171 | /** 172 | * Ensures a string cannot be used as part of an XSS attack. 173 | * @param {string} string - Check if a string contains opening or closing HTML tags. 174 | * @returns {Boolean} 175 | */ 176 | xssValid(string) { 177 | try { 178 | if(string.includes("<") || string.includes(">")) { 179 | return false; 180 | } 181 | return true; 182 | } catch(error) { 183 | return false; 184 | } 185 | }, 186 | 187 | /** 188 | * Generate a random and available username from a list of 64 words. 189 | * @param {Object} clients - A list of clients. 190 | * @returns {string} - The randomly generated username. 191 | */ 192 | getUsername(clients) { 193 | let words = require("./Words"); 194 | let available = Object.keys(words); 195 | 196 | let ips = Object.keys(clients); 197 | ips.map(ip => { 198 | let index = available.indexOf([clients[ip]["username"]]); 199 | if(index > -1) { 200 | available.splice(index, 1); 201 | } 202 | }); 203 | 204 | let max = available.length - 1; 205 | 206 | let random = this.randomBetween(0, max); 207 | 208 | return words[available[random]]; 209 | }, 210 | 211 | /** 212 | * Generate a random and available array of colors from a list of 64 arrays. 213 | * @param {string} address - The IP address of the client. 214 | * @param {Object} clients - A list of clients. 215 | * @returns {Object} - An object containing the array of colors and its index. 216 | */ 217 | getColor(address, clients) { 218 | let colors = require("./Colors"); 219 | let available = Object.keys(colors); 220 | 221 | delete clients[address]; 222 | 223 | let ips = Object.keys(clients); 224 | ips.map(ip => { 225 | let index = available.indexOf([clients[ip]["color"]].toString()); 226 | if(index > -1) { 227 | available.splice(index, 1); 228 | } 229 | }); 230 | 231 | let max = available.length - 1; 232 | 233 | let random = this.randomBetween(0, max); 234 | 235 | return { colors:colors[available[random]], index:available[random] }; 236 | }, 237 | 238 | /** 239 | * Generates a random number within a given range. 240 | * @param {Number} min - The smallest possible number. 241 | * @param {Number} max - The largest possible number. 242 | * @returns {Number} - The random number within the given range. 243 | */ 244 | randomBetween(min, max) { 245 | return min + Math.floor(Math.random() * (max - min + 1)); 246 | }, 247 | 248 | /** 249 | * Remove a key from an object without modifying the original object. 250 | * @param {string} key - The key to remove. 251 | * @param {Object} {} - The object to remove the key from. 252 | * @returns {Object} - The object with the key removed. 253 | */ 254 | removeKey(key, {[key]: _, ...rest}) { 255 | return rest; 256 | }, 257 | 258 | /** 259 | * Get the current UNIX timestamp (in seconds). 260 | * @returns {Number} - Current UNIX timestamp. 261 | */ 262 | epoch() { 263 | return Math.floor(new Date().getTime() / 1000); 264 | }, 265 | 266 | /** 267 | * Checks to see if a value is empty. 268 | * @param {any} value - A value to check and see if it's empty. 269 | * @returns {Boolean} 270 | */ 271 | empty(value) { 272 | if(typeof value === "object" && value !== null && Object.keys(value).length === 0) { 273 | return true; 274 | } 275 | if(value === null || typeof value === "undefined" || value.toString().trim() === "") { 276 | return true; 277 | } 278 | return false; 279 | } 280 | } -------------------------------------------------------------------------------- /src/modules/Words.js: -------------------------------------------------------------------------------- 1 | // Words used as random usernames for clients. 2 | const words = { 3 | 0: "Strawberry", 4 | 1: "Muffin", 5 | 2: "Sundae", 6 | 3: "Milkshake", 7 | 4: "Toffee", 8 | 5: "Caramel", 9 | 6: "Chocolate", 10 | 7: "Sugar", 11 | 8: "Apple", 12 | 9: "Peach", 13 | 10: "Lemon", 14 | 11: "Cake", 15 | 12: "Bubbles", 16 | 13: "Bumblebee", 17 | 14: "Pumpkin", 18 | 15: "Sprout", 19 | 16: "Honey", 20 | 17: "Peanut", 21 | 18: "Waffles", 22 | 19: "Kiwi", 23 | 20: "Cocoa", 24 | 21: "Coconut", 25 | 22: "Guava", 26 | 23: "Plum", 27 | 24: "Pineapple", 28 | 25: "Biscuit", 29 | 26: "Cheesecake", 30 | 27: "Cookie", 31 | 28: "Cupcake", 32 | 29: "Donut", 33 | 30: "Sorbet", 34 | 31: "Pudding", 35 | 32: "Jelly", 36 | 33: "Pancake", 37 | 34: "Hazelnut", 38 | 35: "Tiramisu", 39 | 36: "Almond", 40 | 37: "Blueberry", 41 | 38: "Blackberry", 42 | 39: "Cherry", 43 | 40: "Mango", 44 | 41: "Watermelon", 45 | 42: "Raspberry", 46 | 43: "Clementine", 47 | 44: "Mandarin", 48 | 45: "Dragonfruit", 49 | 46: "Mulberry", 50 | 47: "Orange", 51 | 48: "Starfruit", 52 | 49: "Passionfruit", 53 | 50: "Brownie", 54 | 51: "Velvet", 55 | 52: "Gelato", 56 | 53: "Lollipop", 57 | 54: "Mousse", 58 | 55: "Sprinkles", 59 | 56: "Buttermilk", 60 | 57: "Coffee", 61 | 58: "Eclair", 62 | 59: "Mocha", 63 | 60: "Oreo", 64 | 61: "Skittles", 65 | 62: "Vanilla", 66 | 63: "Bubblegum" 67 | }; 68 | 69 | module.exports = words; -------------------------------------------------------------------------------- /src/portableApp.js: -------------------------------------------------------------------------------- 1 | // Imports. 2 | const utils = require("./modules/Utils"); 3 | const port = 3180; 4 | 5 | const electron = require("electron"); 6 | const localShortcut = require("electron-localshortcut"); 7 | const { app, BrowserWindow, shell, ipcMain } = electron; 8 | 9 | const express = require("express"); 10 | const cors = require("cors"); 11 | const path = require("path"); 12 | const portableApp = express(); 13 | 14 | // Use EJS. 15 | portableApp.set("views", path.join(__dirname, "views")); 16 | portableApp.set("view engine", "ejs"); 17 | portableApp.use("/assets", express.static(path.join(__dirname, "assets"))); 18 | 19 | // Use CORS. 20 | portableApp.use(cors()); 21 | 22 | // Since a port cannot be used by two instances of FileDrop, the application is limited to one instance. 23 | app.requestSingleInstanceLock(); 24 | 25 | // Hardware acceleration is disabled as it's not required. This also improves compatibility on some platforms. 26 | app.disableHardwareAcceleration(); 27 | 28 | // Set the name of the Electron application. 29 | app.name = "FileDrop Server"; 30 | 31 | app.on("ready", () => { 32 | // Used to automatically show the developer console. 33 | const debugMode = false; 34 | 35 | // The window width and height. 36 | let windowWidth = 240; 37 | let windowHeight = 210; 38 | 39 | if(debugMode) { 40 | windowWidth += 220; 41 | } 42 | 43 | // The Electron browser window. 44 | const window = new BrowserWindow({ 45 | width: windowWidth, 46 | height: windowHeight, 47 | minWidth: windowWidth, 48 | minHeight: windowHeight, 49 | maxWidth: windowWidth, 50 | maxHeight: windowHeight, 51 | resizable: false, 52 | frame: false, 53 | transparent: true, 54 | x: 80, 55 | y: 80, 56 | webPreferences: { 57 | nodeIntegration: true, 58 | contextIsolation: false 59 | } 60 | }); 61 | 62 | // Closing applications is handled differently in macOS (apps are kept open and are only hidden unless explicitly quit). 63 | if(process.platform === "darwin") { 64 | let quit = true; 65 | 66 | localShortcut.register(window, "Command+Q", () => { 67 | quit = true; 68 | app.quit(); 69 | }); 70 | 71 | localShortcut.register(window, "Command+W", () => { 72 | quit = false; 73 | app.hide(); 74 | }); 75 | 76 | window.on("close", (e) => { 77 | if(!quit) { 78 | e.preventDefault(); 79 | quit = true; 80 | } 81 | }); 82 | } 83 | 84 | window.loadURL(`http://127.0.0.1:${port}/portable`); 85 | 86 | if(debugMode) { 87 | window.webContents.openDevTools(); 88 | } 89 | 90 | portableApp.get("/", (request, response) => { 91 | response.render("index"); 92 | }); 93 | 94 | portableApp.get("/portable", (request, response) => { 95 | let ip = utils.IPv4(request.socket.remoteAddress); 96 | if(ip === "127.0.0.1" || ip === "localhost") { 97 | response.render("portable", { ip:utils.getIP(), port:port }); 98 | } else { 99 | response.end("Access Not Authorized"); 100 | } 101 | }); 102 | 103 | portableApp.get("/forge/prime.worker.js", (request, response) => { 104 | response.sendFile(path.join(__dirname, "./assets/js/prime.worker.min.js")); 105 | }); 106 | 107 | // Allows the user to open the application in their browser by clicking a button on the front-end. 108 | ipcMain.on("open-link", (error, request) => { 109 | try { 110 | shell.openExternal("http://" + request.toString()); 111 | } catch(error) { 112 | console.log(error); 113 | } 114 | }); 115 | 116 | ipcMain.on("quit", () => { 117 | app.quit(); 118 | }); 119 | 120 | // Used for the functionality of the custom frame/title bar. 121 | ipcMain.on("set-window-state", (error, request) => { 122 | let state = request.toString(); 123 | 124 | switch(state) { 125 | case "closed": 126 | (process.platform === "darwin") ? app.hide() : app.quit(); 127 | break; 128 | case "minimized": 129 | window.minimize(); 130 | break; 131 | } 132 | }); 133 | }); 134 | 135 | module.exports = portableApp; -------------------------------------------------------------------------------- /src/server.js: -------------------------------------------------------------------------------- 1 | // Imports. 2 | const utils = require("./modules/Utils"); 3 | const DBManager = require("./modules/DBManager"); 4 | const ConnectionManager = require("./modules/ConnectionManager"); 5 | 6 | // Check if the app is in testing mode (this causes random IPs to be assigned to clients so that unit and integration testing can be done with multiple clients on the same device). 7 | const args = process.argv.slice(2); 8 | let mode = utils.testingMode(args) ? "Test" : ""; 9 | 10 | // Get the local server IP and set the port number. 11 | const ip = utils.getIP(); 12 | const port = 3180; 13 | 14 | // Check if the server is being run as part of the Electron application. 15 | let portable = utils.portableMode(args); 16 | 17 | // If the server is running as part of the Electron application, the database content is stored in the user data directory. 18 | let app, db; 19 | if(portable) { 20 | app = require("./portableApp"); 21 | db = utils.getUserDirectory(); 22 | } else { 23 | app = require("./app"); 24 | db = "db"; 25 | } 26 | 27 | // Start the server. 28 | const server = app.listen(port); 29 | 30 | // Add Socket.IO functionality to the server. 31 | const io = require("socket.io")(server, { 32 | cors: { 33 | origin: "*", 34 | methods: ["GET", "POST", "PUT", "DELETE"] 35 | }, 36 | maxHttpBufferSize: 8192 * 1024 37 | }); 38 | 39 | // Create a new database and clear previous clients in case the DB already exists. 40 | const dbManager = new DBManager(db); 41 | dbManager.clear(); 42 | 43 | // Instantiate the "ConnectionManager" class that's used to manage all client connections. 44 | const connectionManager = new ConnectionManager(io, dbManager, 64, 5000); 45 | 46 | io.on("connection", socket => { 47 | // Attempt to convert the IP address to IPv4 format. 48 | socket.handshake.address = utils.IPv4(socket.handshake.address); 49 | 50 | // If testing mode is enabled, assign a random IP to the client. 51 | if(utils.testingMode(args)) { 52 | socket.handshake.address = utils.randomIP(connectionManager.clients); 53 | } 54 | 55 | // If FileDrop is being run in a Docker container, try to use the X-Forwarded-For header to get the client's IP address. As a fallback, a random IP is assigned (this doesn't affect the functionality of the application). 56 | if(utils.hasDockerEnvironment() || utils.hasDockerGroup()) { 57 | socket.handshake.address = utils.getClientIP(connectionManager.clients, socket); 58 | } 59 | 60 | let address = socket.handshake.address; 61 | 62 | // Ensure the IP address provided by the client is valid and doesn't contain any script tags that could be used as part of an XSS attack since client IP addresses are sometimes shown on the DOM. 63 | if(utils.xssValid(address)) { 64 | // Send the client their IP address as it's stored on the server. 65 | socket.emit("set-ip", address); 66 | 67 | // Generate a random username and send it to the client. 68 | socket.on("random-username", () => { 69 | socket.emit("random-username", utils.getUsername(connectionManager.clients)); 70 | }); 71 | 72 | // Check the validity and availability of the client's username, and register them as a user. 73 | socket.on("register", data => { 74 | if(!utils.validUsername(data.username)) { 75 | socket.emit("username-invalid"); 76 | return; 77 | } 78 | 79 | if(connectionManager.usernameTaken(data.username)) { 80 | socket.emit("username-taken"); 81 | return; 82 | } 83 | 84 | connectionManager.addClient(socket, data.username, data.key); 85 | }); 86 | 87 | // Remove the client as a user, and inform them that they've been logged out. 88 | socket.on("logout", () => { 89 | socket.emit("logout"); 90 | connectionManager.removeClient(address); 91 | }); 92 | } else { 93 | socket.emit("notify", { 94 | title: "Invalid IP", 95 | description: "The provided IP contains invalid characters.", 96 | duration: 4000, 97 | background: "rgb(230,20,20)", 98 | color: "rgb(255,255,255)" 99 | }); 100 | 101 | socket.emit("kick"); 102 | } 103 | }); 104 | 105 | console.log("\x1b[35m", `Started ${mode} Server: `, "\x1b[4m", "http://" + ip + ":" + port, "\x1b[0m"); -------------------------------------------------------------------------------- /src/views/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | FileDrop 34 | 35 | 36 |
37 | 47 | 52 | 65 | 66 | 69 | 103 | 104 | -------------------------------------------------------------------------------- /src/views/portable.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | FileDrop 24 | 25 | 26 |
27 |
28 | 29 | 30 |
31 | 32 |
33 |
34 | <%= ip + ":" + port %> 35 | 36 | 37 |
38 | 39 | -------------------------------------------------------------------------------- /tests/client.test.js: -------------------------------------------------------------------------------- 1 | const puppeteer = require("puppeteer"); 2 | const { exec } = require("child_process"); 3 | const fs = require("fs"); 4 | 5 | const utils = require("../src/modules/Utils"); 6 | 7 | const validKeys = require("./validKeys"); 8 | 9 | let requireStart = true; 10 | 11 | describe("Client Testing", () => { 12 | let storage1 = {}, storage2 = {}; 13 | let browser1, browser2, page1, page2; 14 | 15 | beforeAll(async () => { 16 | if(requireStart) { 17 | exec("node src/server.js testing"); 18 | await new Promise((resolve) => setTimeout(resolve, 3000)); 19 | } 20 | 21 | browser1 = await puppeteer.launch(); 22 | browser2 = await puppeteer.launch(); 23 | 24 | page1 = await browser1.newPage(); 25 | page2 = await browser2.newPage(); 26 | 27 | await page1.goto(`http://${utils.getIP()}:3180`); 28 | await page2.goto(`http://${utils.getIP()}:3180`); 29 | }); 30 | 31 | afterAll(async () => { 32 | exec("kill $(lsof -t -i:3180)"); 33 | 34 | await new Promise((resolve) => setTimeout(resolve, 4000)); 35 | 36 | await browser1.close(); 37 | await browser2.close(); 38 | }); 39 | 40 | describe("Check page title", () => { 41 | test("Should return FileDrop", async () => { 42 | expect(await page1.title()).toEqual("FileDrop"); 43 | expect(await page2.title()).toEqual("FileDrop"); 44 | }); 45 | }); 46 | 47 | describe("Check RSA keys have been generated", () => { 48 | jest.setTimeout(60000); 49 | test("Should take a while but work", async () => { 50 | await page1.waitForSelector(".loading-screen", { hidden:true, timeout:90000 }); 51 | storage1 = await page1.evaluate(() => { 52 | let storage = {}; 53 | storage["publicKey"] = localStorage.getItem("publicKey"); 54 | storage["privateKey"] = localStorage.getItem("privateKey"); 55 | return storage; 56 | }); 57 | 58 | await page2.waitForSelector(".loading-screen", { hidden:true, timeout:90000 }); 59 | storage2 = await page2.evaluate(() => { 60 | let storage = {}; 61 | storage["publicKey"] = localStorage.getItem("publicKey"); 62 | storage["privateKey"] = localStorage.getItem("privateKey"); 63 | return storage; 64 | }); 65 | 66 | expect(storage1["publicKey"]).toContain("PUBLIC"); 67 | expect(storage1["privateKey"]).toContain("PRIVATE"); 68 | 69 | expect(storage2["publicKey"]).toContain("PUBLIC"); 70 | expect(storage2["privateKey"]).toContain("PRIVATE"); 71 | }); 72 | }); 73 | 74 | describe("Login", () => { 75 | jest.setTimeout(120000); 76 | test("Should work", async () => { 77 | await page1.waitForSelector(".loading-screen", { hidden:true, timeout:90000 }); 78 | expect(await page1.evaluate('localStorage.getItem("publicKey")')).not.toBeNull(); 79 | await page1.focus("#input-username"); 80 | await page1.keyboard.type("Username1"); 81 | await page1.waitForTimeout(5000); 82 | expect(await page1.evaluate('document.querySelector("#server-button").textContent')).toContain("Connected"); 83 | await page1.waitForTimeout(500); 84 | expect(await page1.evaluate('document.querySelector("#input-username").value')).not.toEqual(""); 85 | await page1.click("#confirm-username-button"); 86 | await page1.waitForTimeout(2000); 87 | let class1 = await page1.evaluate('document.querySelector("#login-wrapper").getAttribute("class")'); 88 | expect(class1).toContain("hidden"); 89 | 90 | await page2.waitForSelector(".loading-screen", { hidden:true, timeout:90000 }); 91 | expect(await page2.evaluate('localStorage.getItem("publicKey")')).not.toBeNull(); 92 | await page2.focus("#input-username"); 93 | await page2.keyboard.type("Username2"); 94 | await page2.waitForTimeout(5000); 95 | expect(await page2.evaluate('document.querySelector("#server-button").textContent')).toContain("Connected"); 96 | await page2.waitForTimeout(500); 97 | expect(await page2.evaluate('document.querySelector("#input-username").value')).not.toEqual(""); 98 | await page2.click("#confirm-username-button"); 99 | await page2.waitForTimeout(2000); 100 | let class2 = await page2.evaluate('document.querySelector("#login-wrapper").getAttribute("class")'); 101 | expect(class2).toContain("hidden"); 102 | 103 | jest.setTimeout(5000); 104 | }); 105 | }); 106 | 107 | describe("Check client list", () => { 108 | test("Should return Username2", async () => { 109 | await page1.waitForSelector(".username", { timeout:5000 }); 110 | let username = await page1.evaluate('document.getElementsByClassName("username")[0].textContent'); 111 | let action = await page1.evaluate('document.getElementsByClassName("client-action")[0].textContent'); 112 | expect(username).toEqual("Username2"); 113 | expect(action).toEqual("Ask Permission"); 114 | }); 115 | }); 116 | 117 | describe("Ask client 2 for permission to send a file", () => { 118 | test("Should work", async () => { 119 | await page1.evaluate('document.getElementsByClassName("client-action")[0].click()'); 120 | await page2.waitForSelector(".accept", { timeout:5000 }); 121 | let html = await page2.evaluate('document.body.innerHTML'); 122 | expect(html).toContain("would like to send you a file"); 123 | }); 124 | }); 125 | 126 | describe("Accept client 1's request", () => { 127 | test("Should work", async () => { 128 | await page2.evaluate('document.getElementsByClassName("accept")[0].click()'); 129 | await new Promise((resolve) => setTimeout(resolve, 2000)); 130 | html = await page1.evaluate('document.body.innerHTML'); 131 | expect(html).toContain("You can now send files to"); 132 | await new Promise((resolve) => setTimeout(resolve, 1000)); 133 | }); 134 | }); 135 | 136 | describe("Send a file to client 2", () => { 137 | test("Should send and download a file", async () => { 138 | await page1.evaluate('document.getElementsByClassName("client-action")[0].click()'); 139 | await new Promise((resolve) => setTimeout(resolve, 500)); 140 | let elementHandle = await page1.$("#upload-file"); 141 | await elementHandle.uploadFile("./src/assets/img/Icon.png"); 142 | await new Promise((resolve) => setTimeout(resolve, 500)); 143 | await page1.evaluate('document.getElementById("upload-file").dispatchEvent(new Event("change"))'); 144 | await new Promise((resolve) => setTimeout(resolve, 500)); 145 | await page1.click("#upload-button"); 146 | await page2.waitForSelector(".live-span", { timeout:3000 }); 147 | let html = await page2.evaluate('document.body.innerHTML'); 148 | expect(html).toContain("You are receiving a file"); 149 | }); 150 | }); 151 | }); -------------------------------------------------------------------------------- /tests/server.test.js: -------------------------------------------------------------------------------- 1 | const supertest = require("supertest"); 2 | 3 | const validKeys = require("./validKeys"); 4 | 5 | const utils = require("../src/modules/Utils"); 6 | const DBManager = require("../src/modules/DBManager"); 7 | const ConnectionManager = require("../src/modules/ConnectionManager"); 8 | 9 | const Client = require("socket.io-client"); 10 | 11 | const fs = require("fs"); 12 | 13 | const ip = utils.getIP(); 14 | const port = 3180; 15 | 16 | const app = require("../src/app"); 17 | 18 | const server = app.listen(port); 19 | 20 | const io = require("socket.io")(server, { 21 | cors: { 22 | origin: "*", 23 | methods: ["GET", "POST", "PUT", "DELETE"] 24 | }, 25 | maxHttpBufferSize: 8192 * 1024 26 | }); 27 | 28 | const dbManager = new DBManager("testDB"); 29 | 30 | describe("app.js GET /", () => { 31 | test("Should return 200", async () => { 32 | let response = await supertest(app).get("/"); 33 | expect(response.statusCode).toEqual(200); 34 | }); 35 | }); 36 | 37 | describe("DB Testing", () => { 38 | describe("Check if DB exists", () => { 39 | test("Should return true", async () => { 40 | expect(fs.existsSync("testDB")).toBeTruthy(); 41 | }); 42 | }); 43 | 44 | describe("Test save() method", () => { 45 | test("Should return true", async () => { 46 | let response = await dbManager.save("testingKey", "testingValue", true); 47 | expect(response.ok).toEqual(true); 48 | }); 49 | }); 50 | 51 | describe("Test exists() method", () => { 52 | test("Should return true", async () => { 53 | let response = await dbManager.exists("testingKey"); 54 | expect(response).toEqual(true); 55 | }); 56 | }); 57 | 58 | describe("Test fetch() method", () => { 59 | test("Should return testingValue", async () => { 60 | let response = await dbManager.fetch("testingKey"); 61 | expect(response.data).toEqual("testingValue"); 62 | }); 63 | }); 64 | 65 | describe("Test save() method for updating", () => { 66 | test("Should return true", async () => { 67 | let response = await dbManager.save("testingKey", "newValue", true); 68 | expect(response.ok).toEqual(true); 69 | }); 70 | }); 71 | 72 | describe("Test fetch() method for updated value", () => { 73 | test("Should return newValue", async () => { 74 | let response = await dbManager.fetch("testingKey"); 75 | expect(response.data).toEqual("newValue"); 76 | }); 77 | }); 78 | 79 | describe("Test remove() method", () => { 80 | test("Should return true", async () => { 81 | let response = await dbManager.remove("testingKey"); 82 | expect(response.ok).toEqual(true); 83 | }); 84 | }); 85 | 86 | describe("Destroy the DB", () => { 87 | test("Should return false", async () => { 88 | await dbManager.db.destroy(); 89 | expect(fs.existsSync("testDB")).toEqual(false); 90 | }); 91 | }); 92 | }); 93 | 94 | describe("API Testing", () => { 95 | let dbManager, connectionManager, client1, client2, client3, client4; 96 | let testClients = []; 97 | 98 | beforeAll((done) => { 99 | dbManager = new DBManager("testDB"); 100 | connectionManager = new ConnectionManager(io, dbManager, 3, false); 101 | 102 | client1 = new Client(`http://${ip}:${port}`); 103 | client2 = new Client(`http://${ip}:${port}`); 104 | client3 = new Client(`http://${ip}:${port}`); 105 | client4 = new Client(`http://${ip}:${port}`); 106 | 107 | io.on("connection", socket => { 108 | if(testClients.length === 0) { 109 | socket.handshake.address = "0"; 110 | } 111 | 112 | if(testClients.length === 1) { 113 | socket.handshake.address = "1"; 114 | } 115 | 116 | if(testClients.length === 2) { 117 | socket.handshake.address = "2"; 118 | } 119 | 120 | if(testClients.length === 3) { 121 | socket.handshake.address = "3"; 122 | } 123 | 124 | testClients.push(socket); 125 | 126 | if(testClients.length === 4) { 127 | done(); 128 | } 129 | }); 130 | }); 131 | 132 | afterAll(async () => { 133 | client1.close(); 134 | client2.close(); 135 | client3.close(); 136 | client4.close(); 137 | io.close(); 138 | server.close(); 139 | await dbManager.db.destroy(); 140 | }); 141 | 142 | describe("ConnectionManager Tests", () => { 143 | describe("Check if client list is empty", () => { 144 | test("Should return 0", async () => { 145 | expect(Object.keys(connectionManager.clients).length).toEqual(0); 146 | }); 147 | }); 148 | 149 | describe("Check that 4 clients are connected", () => { 150 | test("Should return 4", async () => { 151 | expect(testClients.length).toEqual(4); 152 | }); 153 | }); 154 | 155 | describe("Test the attach() method", () => { 156 | test("Should work", async () => { 157 | expect(connectionManager.attach(testClients[0])).toEqual(undefined); 158 | expect(connectionManager.attach(testClients[1])).toEqual(undefined); 159 | expect(connectionManager.attach(testClients[2])).toEqual(undefined); 160 | }); 161 | }); 162 | 163 | describe("Test the addClient() method with a wrong key", () => { 164 | test("Should not add any clients", done => { 165 | connectionManager.addClient(testClients[0], "TestUsername1", "<>"); 166 | expect(Object.keys(connectionManager.clients).length).toEqual(0); 167 | done(); 168 | }); 169 | }); 170 | 171 | describe("Test the addClient() method with a valid key", () => { 172 | test("Should add both clients", done => { 173 | connectionManager.addClient(testClients[0], "TestUsername1", validKeys["0"].publicKey); 174 | connectionManager.addClient(testClients[1], "TestUsername2", validKeys["1"].publicKey); 175 | connectionManager.addClient(testClients[2], "TestUsername3", validKeys["2"].publicKey); 176 | expect(Object.keys(connectionManager.clients).length).toEqual(3); 177 | done(); 178 | }); 179 | }); 180 | 181 | describe("Test the clientLimitReached() method by tryng to add another client", () => { 182 | test("Should still have 3 clients", done => { 183 | connectionManager.addClient(testClients[3], "TestUsername4", validKeys["0"].publicKey); 184 | expect(Object.keys(connectionManager.clients).length).toEqual(3); 185 | done(); 186 | }); 187 | }); 188 | 189 | describe("Test the removeClient() method", () => { 190 | test("Should leave 2 clients", done => { 191 | connectionManager.removeClient(testClients[2].handshake.address); 192 | expect(Object.keys(connectionManager.clients).length).toEqual(2); 193 | done(); 194 | }); 195 | }); 196 | 197 | describe("Test the usernameTaken() method", () => { 198 | test("Should return true", async () => { 199 | expect(connectionManager.usernameTaken("TestUsername1")).toEqual(true); 200 | }); 201 | }); 202 | 203 | describe("Test the clientExists() method", () => { 204 | test("Should return true, then false", async () => { 205 | expect(connectionManager.clientExists(testClients[1].handshake.address)).toEqual(true); 206 | expect(connectionManager.clientExists(testClients[3].handshake.address)).toEqual(false); 207 | }); 208 | }); 209 | }); 210 | 211 | describe("PermissionManager Tests", () => { 212 | describe("Test if client 1 can send client 2 a request to send files", () => { 213 | test("Should work", done => { 214 | client2.on("ask-permission", data => { 215 | expect(data).not.toBeNull(); 216 | done(); 217 | }); 218 | 219 | client1.emit("ask-permission", { from:testClients[0].handshake.address, to:testClients[1].handshake.address }); 220 | }); 221 | }); 222 | 223 | describe("Test if client 1 can whitelist client 2", () => { 224 | test("Should work", done => { 225 | client1.on("update-permission", data => { 226 | expect(data).not.toBeNull(); 227 | done(); 228 | }); 229 | 230 | client1.emit("update-permission", { to:"1", whitelist:{ ["0"]: { allowed:true }, response:true }}); 231 | client2.emit("update-permission", { to:"0", whitelist:{ ["1"]: { allowed:true }, response:true }}); 232 | }); 233 | }); 234 | }); 235 | }); -------------------------------------------------------------------------------- /tests/validKeys.js: -------------------------------------------------------------------------------- 1 | const validKeys = { 2 | 0: { 3 | publicKey: `-----BEGIN PUBLIC KEY----- 4 | MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA0z3+bTc8bZbnjQVA6PkG 5 | WtIvs4f7mzD5R5Krz7fdhvfXsjiGNmT/bUax78AhPa+10QT0efoRVUhAlsfT2XkO 6 | vZzcd+B9cEt6KOo+yESJvM6KkkKmq9i80V+8CwEPbPdJXs0s27nXWDGhJHavSVNG 7 | 79iq0DPVudT50kF1L9e0C21+EUSRhDVAXd1n5Kb/H+DvX1xDSXbl6igLD+g3suTF 8 | t/ruzw4SmY1hmFC4EsNULdrYxLneoklXBpc1IG41VsaeDucQMy/m6MTQMEzwVJ7S 9 | mzm4aOu83YSpvt5Ohg8PpqV78GtEc+ABDkJe2WOlE+583q0FWl3ZLYXDYk8/6etv 10 | W0xLwDmDQ5BYM3hnv3YmOTZ1SbP3G215Op2qpxs6SUASmsNwSGTxwOqDJDd6desG 11 | Y8L7dG73PGNdxaoxsfARMHzh8F1GCprs7Twju3xVFI1Cn8JA3poZ77TE7Q75ZsGg 12 | 5eYVEzAizn9q9ZbDk6yu0sEhp1Dx7k4nxftML/WNICMIam7sNil3yB1XDuwHCUYy 13 | XiUyS1kV76VhxR9VFrxnDlTFWXhlRk/KiZCTDhRaVMHJMx2TIrTjEPfbOUOguxvJ 14 | vw8hgyrPNqJl8chANqaASWJLQoYNmqqmfK79V4OlJJmzhNpgdN9vG0HaoL+EtnX8 15 | v3Fz7Q/cnaLtkUt3UXSsqP8CAwEAAQ== 16 | -----END PUBLIC KEY-----`, 17 | privateKey: `-----BEGIN RSA PRIVATE KEY----- 18 | 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 67 | -----END RSA PRIVATE KEY-----`, 68 | }, 69 | 1: { 70 | publicKey: `-----BEGIN PUBLIC KEY----- 71 | MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAmFZvIbW0Uw5yPAv9k6gs 72 | QzrKrIaZ58FhIlonXdB/ucLs4ok6bwUauvDhz9gX5cF6U7VL4Xh2yA22WI5ih5b2 73 | PTF50BADfDFQltg1yVHJ+rBoNJrkw0g8Xv0cFXoR6yRTdeyi4WpLigntzF2PH2VV 74 | egf3UfC0klPtw+dHUwanYQkOgOpLBYExiLY2hDX748IQB8JST8T81xxrWLlp3220 75 | PEXGIT8s0sXpQHRfQILHiv6P2HjjviRmCA80fK30uq6kH4y9eXypfl3a79A4JIye 76 | 2R4Vocutg3Iv9vqe0e7DPk7hYC39URfNGM+6WtshyIvtPiO5jiZmFeyWd1VaS7h1 77 | 82dCR3+BbybtFckF69C8k81AfLPAlGcDZ6WlCoYjpwa2DHMGuZOGdijiiGey7I9/ 78 | /OFc9GJf7+1EfU0c2PAWrHjrZhGGv7z2HKxTc3SFgkIFEX1WYcrUTSXBILw0Ej6J 79 | u9uWPhwx+o7jn2q77d/RAr4JlD1Bbk24mMswKwloLs7mRrhtp/h4dkrZpmFKy42U 80 | rrBFFu4ab7jmQDzngj7x0sAB5Dv5xL/HgCh0r5A6O5+RyQKFWbGAi0bWuWGReVex 81 | BHhYg53Ooaqe6IlCuUB6h+vOuKqm42e9wAxwVdpTXU+5Ac+BWnqoCy7ra9E1xwki 82 | YUoVL2l5Own8Nv69tVXznTsCAwEAAQ== 83 | -----END PUBLIC KEY-----`, 84 | privateKey: `-----BEGIN RSA PRIVATE KEY----- 85 | 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 134 | -----END RSA PRIVATE KEY-----` 135 | }, 136 | 2: { 137 | publicKey: `-----BEGIN PUBLIC KEY----- 138 | MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAqmhr65tCAyjNx30Qkzd8 139 | C8YKp+VnTPGLf+r8UTspszEJdbowgz7ZugH0W1oUqnUMbcB5krfRuftRWFdRPyp9 140 | Jdsooz2AKj9olx167mCgEDoO7EfNcNzL3sBEt0u1Gxvn7Wpq0FSpkcJLd7ztT3pR 141 | vwiifFyupTzoLmJofUxte9qjL3BfWRnLOB076fsXPFLuZqoBD8s9G1S/b1EVME6V 142 | LV58aDTPsKkSHitLuUlhuwLqIT6grfHj3LgpU+y5wGfK63tLFrU/+v2G+facMuOU 143 | RGTW3mYSGzB1LvMtivKmnIyFmKRr0Xdv1WxFPjEgp7xz15BH/8tvo9ptei5oPp5S 144 | U8/k7DZ2LWothAofMzsHgkoytU7R0pAhtKIcGU7AYBIVczy5Zx33LdT87Dx3WBdQ 145 | uMqDjNK503Y1zjaWZtmBpjB7pTx2PnHAK8pGrSRFkEl9KbLHBJjfUWYWnXcoSiTW 146 | 9DhNaOmP0X2AhAKeb5bXUu4Ve4CHeVX9AGXxSzzOf4/ZBjAELihcxbWR1bpCwqwE 147 | QSbjM2OGRCnyByyeQEdjwE/c4j1kLG3XcZAN6jzv9dWrPrw2U8506W97S6s/ssDt 148 | iYmRfto003/63ydx8YZQVRt1X06wwDnfdqeoks0rlogLO23oVQ6XLrgPK+1ihKBe 149 | YPFa8kRgSHO3Sw0zr30pDRcCAwEAAQ== 150 | -----END PUBLIC KEY-----`, 151 | privateKey: `-----BEGIN RSA PRIVATE KEY----- 152 | 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 201 | -----END RSA PRIVATE KEY-----` 202 | } 203 | } 204 | 205 | module.exports = validKeys; --------------------------------------------------------------------------------