From 11f715e1b0b5faf4bb6ecdb5543051517a9c6333 Mon Sep 17 00:00:00 2001 From: Ben Letchford Date: Tue, 22 Oct 2019 15:46:24 +1100 Subject: [PATCH] Downloading of files via wetty terminal (#206) * Feature for download of files via Blob * Document file download feature and bump version. --- README.md | 29 ++++++++++++++++ package.json | 7 ++-- src/client/index.ts | 78 ++++++++++++++++++++++++++++++++++++++++++- src/client/wetty.scss | 6 ++++ yarn.lock | 10 ++++++ 5 files changed, 127 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 5c13055..414a8ce 100644 --- a/README.md +++ b/README.md @@ -134,6 +134,35 @@ the user like this (Only while running wetty as a non root account): This is not a required feature and the security implications for passing the password in the url will have to be considered by the user +### File Downloading + +Wetty supports file downloads by printing terminal escape sequences between a +base64 encoded file. + +The terminal escape sequences used are `^[[5i` and `^[[4i` (VT100 for "enter +auto print" and "exit auto print" respectively - +https://vt100.net/docs/tp83/appendixc.html). + +An example of a helper script that prints the terminal escape characters and +base64s stdin: + +``` +$ cat wetty-download.sh +#!/bin/sh +echo '^[[5i'$(cat /dev/stdin | base64)'^[[4i' +``` + +You are then able to download files via wetty! + +``` +$ cat my-pdf-file.pdf | ./wetty-download.sh +``` + +Wetty will then issue a popup like the following that links to a local file +blob: + +`Download ready: file-20191015233654.pdf` + ## Run wetty behind nginx or apache As said earlier you can use a proxy to add https to WeTTy. diff --git a/package.json b/package.json index b84f84d..82ea695 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "wetty", - "version": "1.1.7", + "version": "1.1.8", "description": "WeTTY = Web + TTY. Terminal access in browser over http/https", "homepage": "https://github.com/krishnasrinivas/wetty", "repository": { @@ -51,6 +51,7 @@ "dependencies": { "compression": "^1.7.4", "express": "^4.17.1", + "file-type": "^12.3.0", "fs-extra": "^8.1.0", "helmet": "^3.20.1", "lodash": "^4.17.15", @@ -60,6 +61,7 @@ "socket.io": "^2.2.0", "socket.io-client": "^2.2.0", "source-map-loader": "^0.2.4", + "toastify-js": "^1.6.1", "winston": "^3.2.1", "xterm": "^3.14.5", "yargs": "^14.0.0" @@ -126,6 +128,7 @@ "Strubbl ", "koushikmln ", "mirtouf ", - "nosemeocurrenada " + "nosemeocurrenada ", + "Ben Letchford " ] } diff --git a/src/client/index.ts b/src/client/index.ts index bd879eb..f8afcce 100644 --- a/src/client/index.ts +++ b/src/client/index.ts @@ -2,6 +2,8 @@ import { Terminal } from 'xterm'; import { isUndefined } from 'lodash'; import * as io from 'socket.io-client'; import { fit } from 'xterm/lib/addons/fit/fit'; +import * as fileType from 'file-type'; +import Toastify from 'toastify-js'; import './wetty.scss'; import './favicon.ico'; @@ -12,8 +14,12 @@ const socket = io(window.location.origin, { path: `${trim(socketBase)}/socket.io`, }); +const FILE_BEGIN = '\u001b[5i'; +const FILE_END = '\u001b[4i'; + socket.on('connect', () => { const term = new Terminal(); + let fileBuffer = []; term.open(document.getElementById('terminal')); const defaultOptions = { fontSize: 14 }; let options: object; @@ -82,6 +88,52 @@ socket.on('connect', () => { disconnect(data); } + function onCompleteFile() { + let bufferCharacters = fileBuffer.join(''); + bufferCharacters = bufferCharacters.substring(bufferCharacters.lastIndexOf(FILE_BEGIN) + FILE_BEGIN.length, bufferCharacters.lastIndexOf(FILE_END)); + + // Try to decode it as base64, if it fails we assume it's not base64 + try { + bufferCharacters = window.atob(bufferCharacters); + } catch (err) { + // Assuming it's not base64... + } + + const bytes = new Uint8Array(bufferCharacters.length); + for (let i = 0; i < bufferCharacters.length; i += 1) { + bytes[i] = bufferCharacters.charCodeAt(i); + } + + let mimeType = 'application/octet-stream'; + let fileExt = ''; + const typeData = fileType(bytes); + if (typeData) { + mimeType = typeData.mime; + fileExt = typeData.ext; + } + const fileName = `file-${new Date() + .toISOString() + .split('.')[0] + .replace(/-/g, '') + .replace('T', '') + .replace(/:/g, '')}${fileExt ? `.${fileExt}` : ''}`; + + const blob = new Blob([new Uint8Array(bytes.buffer)], { type: mimeType }); + const blobUrl = URL.createObjectURL(blob); + + fileBuffer = []; + + Toastify({ + text: `Download ready: ${fileName}`, + duration: 10000, + newWindow: true, + gravity: 'bottom', + position: 'right', + backgroundColor: '#fff', + stopOnFocus: true, + }).showToast(); + } + term.on('data', data => { socket.emit('input', data); }); @@ -90,7 +142,31 @@ socket.on('connect', () => { }); socket .on('data', (data: string) => { - term.write(data); + const indexOfFileBegin = data.indexOf(FILE_BEGIN); + const indexOfFileEnd = data.indexOf(FILE_END); + + // If we've got the entire file in one chunk + if (indexOfFileBegin !== -1 && indexOfFileEnd !== -1) { + fileBuffer.push(data); + onCompleteFile(); + } + // If we've found a beginning marker + else if (indexOfFileBegin !== -1) { + fileBuffer.push(data); + } + // If we've found an ending marker + else if (indexOfFileEnd !== -1) { + fileBuffer.push(data); + onCompleteFile(); + } + // If we've found the continuation of a file + else if (fileBuffer.length > 0) { + fileBuffer.push(data); + } + // Just treat it as normal data + else { + term.write(data); + } }) .on('login', () => { term.writeln(''); diff --git a/src/client/wetty.scss b/src/client/wetty.scss index c8ae18c..aa5816a 100644 --- a/src/client/wetty.scss +++ b/src/client/wetty.scss @@ -1,4 +1,5 @@ @import '~xterm/dist/xterm'; +@import '~toastify-js/src/toastify.css'; $black: #000; $grey: rgba(0, 0, 0, 0.75); @@ -94,4 +95,9 @@ body { display: flex; } } + + .toastify { + border-radius: 0; + color: $black; + } } diff --git a/yarn.lock b/yarn.lock index 93a4328..99316e4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3152,6 +3152,11 @@ file-loader@^4.2.0: loader-utils "^1.2.3" schema-utils "^2.0.0" +file-type@^12.3.0: + version "12.3.0" + resolved "https://registry.yarnpkg.com/file-type/-/file-type-12.3.0.tgz#74d755e5dc9c5cbc7ee6f182529b453906ac88c2" + integrity sha512-4E4Esq9KLwjYCY32E7qSmd0h7LefcniZHX+XcdJ4Wfx1uGJX7QCigiqw/U0yT7WOslm28yhxl87DJ0wHYv0RAA== + fill-range@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7" @@ -7196,6 +7201,11 @@ to-regex@^3.0.1, to-regex@^3.0.2: regex-not "^1.0.2" safe-regex "^1.1.0" +toastify-js@^1.6.1: + version "1.6.1" + resolved "https://registry.yarnpkg.com/toastify-js/-/toastify-js-1.6.1.tgz#2ec20654925d6f83f935d5a6907c146e6bcb67d6" + integrity sha512-yosiXPEdr3B9KL1rF7M/IMdw8d8Z69UJe2JsvxbfdpaL2/olqSB8tvp7/N6gkT9G46y6nqR2e62CCnf0LxeIBQ== + toidentifier@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553"