diff --git a/README.md b/README.md new file mode 100644 index 0000000..0337726 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# Kclient + +ALPHA VERSION NO TOUCH diff --git a/index.js b/index.js new file mode 100644 index 0000000..62c7b4e --- /dev/null +++ b/index.js @@ -0,0 +1,136 @@ +// LinuxServer KasmVNC Client + +//// Env variables //// +var CUSTOM_USER = process.env.CUSTOM_USER || 'abc'; +var PASSWORD = process.env.PASSWORD || 'abc'; +var SUBFOLDER = process.env.SUBFOLDER || '/'; +var TITLE = process.env.TITLE || 'KasmVNC Client'; +var FM_HOME = process.env.FM_HOME || '/config'; + +//// Application Variables //// +var socketIO = require('socket.io'); +var express = require('express'); +var ejs = require('ejs'); +var app = require('express')(); +var http = require('http').Server(app); +var bodyParser = require('body-parser'); +var baseRouter = express.Router(); +var fsw = require('fs').promises; +var fs = require('fs'); + + +//// Server Paths Main //// +app.engine('html', require('ejs').renderFile); +app.engine('json', require('ejs').renderFile); +baseRouter.use('/public', express.static(__dirname + '/public')); +baseRouter.use('/vnc', express.static("/usr/share/kasmvnc/www/")); +baseRouter.get('/', function (req, res) { + res.render(__dirname + '/public/index.html', {title: TITLE}); +}); +baseRouter.get('/favicon.ico', function (req, res) { + res.sendFile(__dirname + '/public/favicon.ico'); +}); +baseRouter.get('/manifest.json', function (req, res) { + res.render(__dirname + '/public/manifest.json', {title: TITLE}); +}); + +//// Web File Browser //// +// Send landing page +baseRouter.get('/files', function (req, res) { + res.sendFile( __dirname + '/public/filebrowser.html'); +}); +// Websocket comms // +io = socketIO(http, {path: SUBFOLDER + 'files/socket.io',maxHttpBufferSize: 200000000}); +io.on('connection', async function (socket) { + let id = socket.id; + + //// Functions //// + + // Open default location + async function checkAuth(password) { + getFiles(FM_HOME); + } + + // Emit to user + function send(command, data) { + io.sockets.to(id).emit(command, data); + } + + // Get file list for directory + async function getFiles(directory) { + let items = await fsw.readdir(directory); + if (items.length > 0) { + let dirs = []; + let files = []; + for await (let item of items) { + let fullPath = directory + '/' + item; + if (fs.lstatSync(fullPath).isDirectory()) { + dirs.push(item); + } else { + files.push(item); + } + } + send('renderfiles', [dirs, files, directory]); + } else { + send('renderfiles', [[], [], directory]); + } + } + + // Send file to client + async function downloadFile(file) { + let fileName = file.split('/').slice(-1)[0]; + let data = await fsw.readFile(file); + send('sendfile', [data, fileName]); + } + + // Write client sent file + async function uploadFile(res) { + let directory = res[0]; + let filePath = res[1]; + let data = res[2]; + let render = res[3]; + let dirArr = filePath.split('/'); + let folder = filePath.replace(dirArr[dirArr.length - 1], '') + await fsw.mkdir(folder, { recursive: true }); + await fsw.writeFile(filePath, Buffer.from(data)); + if (render) { + getFiles(directory); + } + } + + // Delete files + async function deleteFiles(res) { + let item = res[0]; + let directory = res[1]; + item = item.replace("|","'"); + if (fs.lstatSync(item).isDirectory()) { + await fsw.rm(item, {recursive: true}); + } else { + await fsw.unlink(item); + } + getFiles(directory); + } + + // Create a folder + async function createFolder(res) { + let dir = res[0]; + let directory = res[1]; + if (!fs.existsSync(dir)){ + await fsw.mkdir(dir); + } + getFiles(directory); + } + + // Incoming socket requests + socket.on('open', checkAuth); + socket.on('getfiles', getFiles); + socket.on('downloadfile', downloadFile); + socket.on('uploadfile', uploadFile); + socket.on('deletefiles', deleteFiles); + socket.on('createfolder', createFolder); +}); + + +// Spin up application on 6900 +app.use(SUBFOLDER, baseRouter); +http.listen(6900); diff --git a/package.json b/package.json new file mode 100644 index 0000000..cac04fe --- /dev/null +++ b/package.json @@ -0,0 +1,31 @@ +{ + "name": "kclient", + "version": "0.1.0", + "description": "Kclient is a wrapper for KasmVNC to add functionality to a containerized environment", + "main": "index.js", + "dependencies": { + "ejs": "^3.1.8", + "express": "^4.18.2", + "socket.io": "^4.6.0" + }, + "devDependencies": {}, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/linuxserver/kclient.git" + }, + "keywords": [ + "VNC", + "Webtop", + "VDI", + "Docker" + ], + "author": "thelamer", + "license": "GPL-3.0-or-later", + "bugs": { + "url": "https://github.com/linuxserver/kclient/issues" + }, + "homepage": "https://github.com/linuxserver/kclient#readme" +} diff --git a/public/css/filebrowser.css b/public/css/filebrowser.css new file mode 100644 index 0000000..0fb5a1d --- /dev/null +++ b/public/css/filebrowser.css @@ -0,0 +1,72 @@ +html * { + font-family: Poppins,Helvetica !important; + color: white !important; +} + +.hidden { + display: none; +} + +.right { + float: right; + margin-right: 5px; +} + +.directory, .file { + cursor: pointer; +} + +button { + background-color: rgb(9 2 2 / 0.6); + border-radius: 5px; + border-style: inset; + border-color: rgb(255 255 255 / 0.6); + cursor: pointer; + margin: 5px; +} + +.deleteButton { + margin: 0px !important; + float: right; +} + +.fileTable { + border-collapse: collapse; + width: 100%; + margin-top: 10px; +} + +td, th { + border: 2px solid #ddd; + padding: 8px; +} + +tr:hover, button:hover { + background: rgba(255, 255, 255, 0.3) +} + +#dropzone { + position: fixed; top: 0; left: 0; + z-index: 9999999999; + width: 100%; height: 100%; + background-color: rgba(0,0,0,0.5); + transition: visibility 175ms, opacity 175ms; +} + +#loading { + display: inline-block; + width: 50px; + height: 50px; + border: 3px solid rgba(0,0,0,.3); + border-radius: 50%; + border-top-color: black; + animation: spin 1s ease-in-out infinite; + -webkit-animation: spin 1s ease-in-out infinite; +} + +@keyframes spin { + to { -webkit-transform: rotate(360deg); } +} +@-webkit-keyframes spin { + to { -webkit-transform: rotate(360deg); } +} diff --git a/public/css/files.svg b/public/css/files.svg new file mode 100644 index 0000000..6dc8302 --- /dev/null +++ b/public/css/files.svg @@ -0,0 +1,2 @@ + + diff --git a/public/css/kclient.css b/public/css/kclient.css new file mode 100644 index 0000000..d564244 --- /dev/null +++ b/public/css/kclient.css @@ -0,0 +1,74 @@ +.vnc { + position: fixed; + top: 0; + left: 0; + bottom: 0; + right: 0; + width: 100%; + height: 100%; + border: none; + margin: 0; + padding: 0; + overflow: hidden; +} + +#files { + display: none; + position: absolute; + left: 20vw; + top: 50%; + transform: translateY(-50%); + width: 60vw; + height: 60vh; + z-index: 2; + background-color: rgb(9 2 2 / 0.6); + border-radius: 10px; + border-style: inset; + border-color: rgb(255 255 255 / 0.6); +} + +#files_frame { + width: 100%; + height: 100%; +} + +.close { + position: absolute; + background: DimGray; + top: -10px; + right: -10px; + cursor: pointer; + border-radius:50%; + border-style: inset; + border-color: rgb(255 255 255 / 0.6); + width: 20px; + height: 20px; +} + +#lsbar { + position: absolute; + top: 0; + left: 0; + right: 0; + margin-left: auto; + margin-right: auto; + width: max-content; + display: none; + background-color: rgb(9 2 2 / 0.6); + border-radius: 0 0 10px 10px; + border-style: inset; + border-color: rgb(255 255 255 / 0.6); +} + +.icons { + margin: 5px; + padding: 4px; + height: 4vh; + cursor: pointer; + border-radius: 3px; + filter: invert(100%) sepia(0%) saturate(0%) hue-rotate(82deg) brightness(105%) contrast(105%); +} + +.icons:hover { + background: rgba(0, 0, 0, 0.3); +} diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000..6fe0291 Binary files /dev/null and b/public/favicon.ico differ diff --git a/public/filebrowser.html b/public/filebrowser.html new file mode 100644 index 0000000..cdfc904 --- /dev/null +++ b/public/filebrowser.html @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + +
+ + + diff --git a/public/icon.png b/public/icon.png new file mode 100644 index 0000000..cd8eae7 Binary files /dev/null and b/public/icon.png differ diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..81120e4 --- /dev/null +++ b/public/index.html @@ -0,0 +1,29 @@ + + + + + + <%- title -%> + + + + + + + + + + +
+ +
+ +
+ +
+
+ + + + + diff --git a/public/js/filebrowser.js b/public/js/filebrowser.js new file mode 100644 index 0000000..76f9eb1 --- /dev/null +++ b/public/js/filebrowser.js @@ -0,0 +1,262 @@ +var host = window.location.hostname; +var port = window.location.port; +var protocol = window.location.protocol; +var path = window.location.pathname; +var socket = io(protocol + '//' + host + ':' + port, { path: path + '/socket.io'}); + +// Open default folder on connect +socket.on('connect',function(){ + $('#filebrowser').empty(); + $('#filebrowser').append($('
').attr('id','loading')); + socket.emit('open', ''); +}); + +// Get file list +function getFiles(directory) { + directory = directory.replace("//","/"); + directory = directory.replace("|","'"); + let directoryClean = directory.replace("'","|"); + if ((directory !== '/') && (directory.endsWith('/'))) { + directory = directory.slice(0, -1); + } + $('#filebrowser').empty(); + $('#filebrowser').append($('
').attr('id','loading')); + socket.emit('getfiles', directory); +} + +// Render file list +async function renderFiles(data) { + let dirs = data[0]; + let files = data[1]; + let directory = data[2]; + let baseName = directory.split('/').slice(-1)[0]; + let parentFolder = directory.replace(baseName,''); + let parentLink = $('').addClass('directory').attr('onclick', 'getFiles(\'' + parentFolder + '\');').text('..'); + let directoryClean = directory.replace("'","|"); + if (directoryClean == '/') { + directoryClean = ''; + } + let table = $('').addClass('fileTable'); + let tableHeader = $(''); + for await (name of ['Name', 'Type', 'Delete (NO WARNING)']) { + tableHeader.append($(''); + for await (item of [parentLink, $(''); + let dirClean = dir.replace("'","|"); + let link = $(''); + let fileClean = file.replace("'","|"); + let link = $('
').text(name)); + } + let parentRow = $('
').text('Parent'), $('')]) { + parentRow.append(item); + } + table.append(tableHeader,parentRow); + $('#filebrowser').empty(); + $('#filebrowser').data('directory', directory); + $('#filebrowser').append($('
').text(directory)); + $('#filebrowser').append(table); + if (dirs.length > 0) { + for await (let dir of dirs) { + let tableRow = $('
').addClass('directory').attr('onclick', 'getFiles(\'' + directoryClean + '/' + dirClean + '\');').text(dir); + let type = $('').text('Dir'); + let del = $('').append($('
').addClass('file').attr('onclick', 'downloadFile(\'' + directoryClean + '/' + fileClean + '\');').text(file); + let type = $('').text('File'); + let del = $('').append($('