Browse Source

Downloading of files via wetty terminal (#206)

* Feature for download of files via Blob

* Document file download feature and bump version.
pull/162/head
Ben Letchford 5 years ago
committed by Cian Butler
parent
commit
11f715e1b0
  1. 29
      README.md
  2. 7
      package.json
  3. 78
      src/client/index.ts
  4. 6
      src/client/wetty.scss
  5. 10
      yarn.lock

29
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 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 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 ## Run wetty behind nginx or apache
As said earlier you can use a proxy to add https to WeTTy. As said earlier you can use a proxy to add https to WeTTy.

7
package.json

@ -1,6 +1,6 @@
{ {
"name": "wetty", "name": "wetty",
"version": "1.1.7", "version": "1.1.8",
"description": "WeTTY = Web + TTY. Terminal access in browser over http/https", "description": "WeTTY = Web + TTY. Terminal access in browser over http/https",
"homepage": "https://github.com/krishnasrinivas/wetty", "homepage": "https://github.com/krishnasrinivas/wetty",
"repository": { "repository": {
@ -51,6 +51,7 @@
"dependencies": { "dependencies": {
"compression": "^1.7.4", "compression": "^1.7.4",
"express": "^4.17.1", "express": "^4.17.1",
"file-type": "^12.3.0",
"fs-extra": "^8.1.0", "fs-extra": "^8.1.0",
"helmet": "^3.20.1", "helmet": "^3.20.1",
"lodash": "^4.17.15", "lodash": "^4.17.15",
@ -60,6 +61,7 @@
"socket.io": "^2.2.0", "socket.io": "^2.2.0",
"socket.io-client": "^2.2.0", "socket.io-client": "^2.2.0",
"source-map-loader": "^0.2.4", "source-map-loader": "^0.2.4",
"toastify-js": "^1.6.1",
"winston": "^3.2.1", "winston": "^3.2.1",
"xterm": "^3.14.5", "xterm": "^3.14.5",
"yargs": "^14.0.0" "yargs": "^14.0.0"
@ -126,6 +128,7 @@
"Strubbl <github@linux4tw.de>", "Strubbl <github@linux4tw.de>",
"koushikmln <mln02koushik@gmail.com>", "koushikmln <mln02koushik@gmail.com>",
"mirtouf <mirtouf@gmail.com>", "mirtouf <mirtouf@gmail.com>",
"nosemeocurrenada <nosemeocurrenada93@gmail.com>" "nosemeocurrenada <nosemeocurrenada93@gmail.com>",
"Ben Letchford <contact@benl.com.au>"
] ]
} }

78
src/client/index.ts

@ -2,6 +2,8 @@ import { Terminal } from 'xterm';
import { isUndefined } from 'lodash'; import { isUndefined } from 'lodash';
import * as io from 'socket.io-client'; import * as io from 'socket.io-client';
import { fit } from 'xterm/lib/addons/fit/fit'; import { fit } from 'xterm/lib/addons/fit/fit';
import * as fileType from 'file-type';
import Toastify from 'toastify-js';
import './wetty.scss'; import './wetty.scss';
import './favicon.ico'; import './favicon.ico';
@ -12,8 +14,12 @@ const socket = io(window.location.origin, {
path: `${trim(socketBase)}/socket.io`, path: `${trim(socketBase)}/socket.io`,
}); });
const FILE_BEGIN = '\u001b[5i';
const FILE_END = '\u001b[4i';
socket.on('connect', () => { socket.on('connect', () => {
const term = new Terminal(); const term = new Terminal();
let fileBuffer = [];
term.open(document.getElementById('terminal')); term.open(document.getElementById('terminal'));
const defaultOptions = { fontSize: 14 }; const defaultOptions = { fontSize: 14 };
let options: object; let options: object;
@ -82,6 +88,52 @@ socket.on('connect', () => {
disconnect(data); 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: <a href="${blobUrl}" target="_blank" download="${fileName}">${fileName}</a>`,
duration: 10000,
newWindow: true,
gravity: 'bottom',
position: 'right',
backgroundColor: '#fff',
stopOnFocus: true,
}).showToast();
}
term.on('data', data => { term.on('data', data => {
socket.emit('input', data); socket.emit('input', data);
}); });
@ -90,7 +142,31 @@ socket.on('connect', () => {
}); });
socket socket
.on('data', (data: string) => { .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', () => { .on('login', () => {
term.writeln(''); term.writeln('');

6
src/client/wetty.scss

@ -1,4 +1,5 @@
@import '~xterm/dist/xterm'; @import '~xterm/dist/xterm';
@import '~toastify-js/src/toastify.css';
$black: #000; $black: #000;
$grey: rgba(0, 0, 0, 0.75); $grey: rgba(0, 0, 0, 0.75);
@ -94,4 +95,9 @@ body {
display: flex; display: flex;
} }
} }
.toastify {
border-radius: 0;
color: $black;
}
} }

10
yarn.lock

@ -3152,6 +3152,11 @@ file-loader@^4.2.0:
loader-utils "^1.2.3" loader-utils "^1.2.3"
schema-utils "^2.0.0" 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: fill-range@^4.0.0:
version "4.0.0" version "4.0.0"
resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7" 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" regex-not "^1.0.2"
safe-regex "^1.1.0" 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: toidentifier@1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553" resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553"

Loading…
Cancel
Save