adding initial base logic

This commit is contained in:
thelamer 2023-02-09 13:58:42 -08:00
commit 8a779ff930
14 changed files with 708 additions and 0 deletions

3
README.md Normal file
View File

@ -0,0 +1,3 @@
# Kclient
ALPHA VERSION NO TOUCH

136
index.js Normal file
View File

@ -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);

31
package.json Normal file
View File

@ -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"
}

View File

@ -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); }
}

2
public/css/files.svg Normal file
View File

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="UTF-8"?><svg version="1.1" viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"><path d="m28.11 13.609c-2.7387 0-4.9494 2.2106-4.9494 4.9494v90.883c0 2.7387 2.2106 4.9494 4.9494 4.9494h71.809c2.7387 0 4.9204-2.2106 4.9204-4.9494v-90.883c0-2.7387-2.1817-4.9494-4.9204-4.9494zm7.9595 7.3517h55.861c3.0814 0 5.5572 2.5047 5.5572 5.5861v12.648c0 3.0814-2.4758 5.5572-5.5572 5.5572h-55.861c-3.0814 0-5.5282-2.4758-5.5282-5.5572v-12.648c0-3.0814 2.4468-5.5861 5.5282-5.5861zm21.071 9.0015c-2.3244 0-4.1968 1.592-4.1968 3.5601 0 1.9681 1.8724 3.5311 4.1968 3.5311h15.109c2.3244 0 4.1968-1.563 4.1968-3.5311 0-1.9681-1.8724-3.5601-4.1968-3.5601zm-21.071 22.142h55.861c3.0814 0 5.5572 2.5047 5.5572 5.5861v12.648c0 3.0814-2.4758 5.5572-5.5572 5.5572h-55.861c-3.0814 0-5.5282-2.4758-5.5282-5.5572v-12.648c0-3.0814 2.4468-5.5861 5.5282-5.5861zm21.071 9.0015c-2.3244 0-4.1968 1.592-4.1968 3.5601 0 1.9681 1.8724 3.5311 4.1968 3.5311h15.109c2.3244 0 4.1968-1.5631 4.1968-3.5311 0-1.9681-1.8724-3.5601-4.1968-3.5601zm-21.071 22.142h55.861c3.0814 0 5.5572 2.5047 5.5572 5.5861v12.648c0 3.0814-2.4758 5.5572-5.5572 5.5572h-55.861c-3.0814 0-5.5282-2.4758-5.5282-5.5572v-12.648c0-3.0814 2.4468-5.5861 5.5282-5.5861zm21.071 9.0015c-2.3244 0-4.1968 1.592-4.1968 3.5601 0 1.9681 1.8724 3.5311 4.1968 3.5311h15.109c2.3244 0 4.1968-1.563 4.1968-3.5311 0-1.9681-1.8724-3.5601-4.1968-3.5601z"/></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

74
public/css/kclient.css Normal file
View File

@ -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);
}

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

20
public/filebrowser.html Normal file
View File

@ -0,0 +1,20 @@
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="public/css/filebrowser.css">
<script type="text/javascript" src="public/js/jquery.min.js"></script>
<script src="files/socket.io/socket.io.js"></script>
<script type="text/javascript" src="public/js/filebrowser.js"></script>
</head>
<body>
<span id="buttons">
<input id="folderName" type="text" placeholder="Enter Directory Name"></input>
<button onclick="createFolder()">Create Folder</button>
<button onclick="$('#uploadInput').trigger( 'click' )">Upload Files</button>
<input class="hidden" id="uploadInput" type='file' onchange="upload(this);" multiple>
</span>
<div id="filebrowser"></div>
<div ondrop="dropFiles(event)" ondragover="allowDrop(event)" style="visibility:hidden;opacity:0" id="dropzone">
</div>
</body>
</html>

BIN
public/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

29
public/index.html Normal file
View File

@ -0,0 +1,29 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title><%- title -%></title>
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<link rel="apple-touch-icon" href="icon.png">
<link rel="manifest" href="manifest.json">
<link href="public/css/kclient.css" rel="stylesheet">
</head>
<body>
<!--KasmVNC Iframe-->
<iframe class="vnc" src="vnc/index.html?resize=remote&clipboard_up=true&clipboard_down=true&clipboard_seamless=true&show_control_bar=true"></iframe>
<!--LSIO Function Bar-->
<div id="lsbar">
<img class="icons" title="File Manager" src="public/css/files.svg" onclick="toggle('#files')"/>
</div>
<!--File Browser-->
<div id="files">
<iframe id="files_frame" name="files_frame" src="files" frameborder="0"></iframe>
<div class="close" onclick="closeToggle('#files')"></div>
</div>
<!--Main logic-->
<script src="public/js/jquery.min.js"></script>
<script src="public/js/kclient.js"></script>
</body>
</html>

262
public/js/filebrowser.js Normal file
View File

@ -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($('<div>').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($('<div>').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 = $('<td>').addClass('directory').attr('onclick', 'getFiles(\'' + parentFolder + '\');').text('..');
let directoryClean = directory.replace("'","|");
if (directoryClean == '/') {
directoryClean = '';
}
let table = $('<table>').addClass('fileTable');
let tableHeader = $('<tr>');
for await (name of ['Name', 'Type', 'Delete (NO WARNING)']) {
tableHeader.append($('<th>').text(name));
}
let parentRow = $('<tr>');
for await (item of [parentLink, $('<td>').text('Parent'), $('<td>')]) {
parentRow.append(item);
}
table.append(tableHeader,parentRow);
$('#filebrowser').empty();
$('#filebrowser').data('directory', directory);
$('#filebrowser').append($('<div>').text(directory));
$('#filebrowser').append(table);
if (dirs.length > 0) {
for await (let dir of dirs) {
let tableRow = $('<tr>');
let dirClean = dir.replace("'","|");
let link = $('<td>').addClass('directory').attr('onclick', 'getFiles(\'' + directoryClean + '/' + dirClean + '\');').text(dir);
let type = $('<td>').text('Dir');
let del = $('<td>').append($('<button>').addClass('deleteButton').attr('onclick', 'deleter(\'' + directoryClean + '/' + dirClean + '\');').text('Delete'));
for await (item of [link, type, del]) {
tableRow.append(item);
}
table.append(tableRow);
}
}
if (files.length > 0) {
for await (let file of files) {
let tableRow = $('<tr>');
let fileClean = file.replace("'","|");
let link = $('<td>').addClass('file').attr('onclick', 'downloadFile(\'' + directoryClean + '/' + fileClean + '\');').text(file);
let type = $('<td>').text('File');
let del = $('<td>').append($('<button>').addClass('deleteButton').attr('onclick', 'deleter(\'' + directoryClean + '/' + fileClean + '\');').text('Delete'));
for await (item of [link, type, del]) {
tableRow.append(item);
}
table.append(tableRow);
}
}
}
// Download a file
function downloadFile(file) {
file = file.replace("|","'");
socket.emit('downloadfile', file);
}
// Send buffer to download blob
function sendFile(res) {
let data = res[0];
let fileName = res[1];
let blob = new Blob([data], { type: "application/octetstream" });
let url = window.URL || window.webkitURL;
link = url.createObjectURL(blob);
let a = $("<a />");
a.attr("download", fileName);
a.attr("href", link);
$("body").append(a);
a[0].click();
$("body").remove(a);
}
// Upload files to current directory
async function upload(input) {
let directory = $('#filebrowser').data('directory');
if (directory == '/') {
directoryUp = '';
} else {
directoryUp = directory;
}
if (input.files && input.files[0]) {
$('#filebrowser').empty();
$('#filebrowser').append($('<div>').attr('id','loading'));
for await (let file of input.files) {
let reader = new FileReader();
reader.onload = async function(e) {
let fileName = file.name;
if (e.total < 200000000) {
let data = e.target.result;
$('#filebrowser').append($('<div>').text('Uploading ' + fileName));
if (file == input.files[input.files.length - 1]) {
socket.emit('uploadfile', [directory, directoryUp + '/' + fileName, data, true]);
} else {
socket.emit('uploadfile', [directory, directoryUp + '/' + fileName, data, false]);
}
} else {
$('#filebrowser').append($('<div>').text('File too big ' + fileName));
await new Promise(resolve => setTimeout(resolve, 2000));
socket.emit('getfiles', directory);
}
}
reader.readAsArrayBuffer(file);
}
}
}
// Delete file/folder
function deleter(item) {
let directory = $('#filebrowser').data('directory');
$('#filebrowser').empty();
$('#filebrowser').append($('<div>').attr('id','loading'));
socket.emit('deletefiles', [item, directory]);
}
// Delete file/folder
function createFolder() {
let directory = $('#filebrowser').data('directory');
if (directory == '/') {
directoryUp = '';
} else {
directoryUp = directory;
}
let folderName = $('#folderName').val();
$('#folderName').val('');
if ((folderName.length == 0) || (folderName.includes('/'))) {
alert('Bad or Null Directory Name');
return '';
}
$('#filebrowser').empty();
$('#filebrowser').append($('<div>').attr('id','loading'));
socket.emit('createfolder', [directoryUp + '/' + folderName, directory]);
}
// Handle drag and drop
async function dropFiles(ev) {
ev.preventDefault();
$('#filebrowser').empty();
$('#filebrowser').append($('<div>').attr('id','loading'));
$('#dropzone').css({'visibility':'hidden','opacity':0});
let directory = $('#filebrowser').data('directory');
if (directory == '/') {
directoryUp = '';
} else {
directoryUp = directory;
}
let items = await getAllFileEntries(event.dataTransfer.items);
for await (let item of items) {
let fullPath = item.fullPath;
item.file(async function(file) {
let reader = new FileReader();
reader.onload = async function(e) {
let fileName = file.name;
if (e.total < 200000000) {
let data = e.target.result;
$('#filebrowser').append($('<div>').text('Uploading ' + fileName));
if (item == items[items.length - 1]) {
socket.emit('uploadfile', [directory, directoryUp + '/' + fullPath, data, true]);
} else {
socket.emit('uploadfile', [directory, directoryUp + '/' + fullPath, data, false]);
}
} else {
$('#filebrowser').append($('<div>').text('File too big ' + fileName));
await new Promise(resolve => setTimeout(resolve, 2000));
socket.emit('getfiles', directory);
}
}
reader.readAsArrayBuffer(file);
});
}
}
// Drop handler function to get all files
async function getAllFileEntries(dataTransferItemList) {
let fileEntries = [];
// Use BFS to traverse entire directory/file structure
let queue = [];
// Unfortunately dataTransferItemList is not iterable i.e. no forEach
for (let i = 0; i < dataTransferItemList.length; i++) {
queue.push(dataTransferItemList[i].webkitGetAsEntry());
}
while (queue.length > 0) {
let entry = queue.shift();
if (entry.isFile) {
fileEntries.push(entry);
} else if (entry.isDirectory) {
let reader = entry.createReader();
queue.push(...await readAllDirectoryEntries(reader));
}
}
return fileEntries;
}
// Get all the entries (files or sub-directories) in a directory by calling readEntries until it returns empty array
async function readAllDirectoryEntries(directoryReader) {
let entries = [];
let readEntries = await readEntriesPromise(directoryReader);
while (readEntries.length > 0) {
entries.push(...readEntries);
readEntries = await readEntriesPromise(directoryReader);
}
return entries;
}
// Wrap readEntries in a promise to make working with readEntries easier
async function readEntriesPromise(directoryReader) {
try {
return await new Promise((resolve, reject) => {
directoryReader.readEntries(resolve, reject);
});
} catch (err) {
console.log(err);
}
}
var lastTarget;
// Change style when hover files
window.addEventListener('dragenter', function(ev) {
lastTarget = ev.target;
$('#dropzone').css({'visibility':'','opacity':1});
});
// Change style when leave hover files
window.addEventListener("dragleave", function(ev) {
if(ev.target == lastTarget || ev.target == document) {
$('#dropzone').css({'visibility':'hidden','opacity':0});
}
});
// Disabled default drag and drop
function allowDrop(ev) {
ev.preventDefault();
}
// Incoming socket requests
socket.on('renderfiles', renderFiles);
socket.on('sendfile', sendFile);

2
public/js/jquery.min.js vendored Normal file

File diff suppressed because one or more lines are too long

60
public/js/kclient.js Normal file
View File

@ -0,0 +1,60 @@
// Parse messages from KasmVNC
var eventMethod = window.addEventListener ? "addEventListener" : "attachEvent";
var eventer = window[eventMethod];
var messageEvent = eventMethod == "attachEvent" ? "onmessage" : "message";
eventer(messageEvent,function(e) {
if (event.data && event.data.action) {
switch (event.data.action) {
case 'control_open':
openToggle('#lsbar');
break;
case 'control_close':
closeToggle('#lsbar');
break;
case 'fullscreen':
fullscreen();
break;
}
}
},false);
// Handle Toggle divs
function openToggle(id) {
if ($(id).is(":hidden")) {
$(id).slideToggle(300);
}
}
function closeToggle(id) {
if ($(id).is(":visible")) {
$(id).slideToggle(300);
}
}
function toggle(id) {
$(id).slideToggle(300);
}
// Fullscreen handler
function fullscreen() {
if (document.fullscreenElement || document.mozFullScreenElement || document.webkitFullscreenElement || document.msFullscreenElement) {
if (document.exitFullscreen) {
document.exitFullscreen();
} else if (document.mozCancelFullScreen) {
document.mozCancelFullScreen();
} else if (document.webkitExitFullscreen) {
document.webkitExitFullscreen();
} else if (document.msExitFullscreen) {
document.msExitFullscreen();
}
} else {
if (document.documentElement.requestFullscreen) {
document.documentElement.requestFullscreen();
} else if (document.documentElement.mozRequestFullScreen) {
document.documentElement.mozRequestFullScreen();
} else if (document.documentElement.webkitRequestFullscreen) {
document.documentElement.webkitRequestFullscreen(Element.ALLOW_KEYBOARD_INPUT);
} else if (document.body.msRequestFullscreen) {
document.body.msRequestFullscreen();
}
}
}

17
public/manifest.json Normal file
View File

@ -0,0 +1,17 @@
{
"name": "<%- title -%>",
"short_name": "<%- title -%>",
"manifest_version": 2,
"version": "1.0",
"display": "fullscreen",
"background_color": "#000000",
"theme_color": "#000000",
"icons": [
{
"src": "public/icon.png",
"type": "image/png",
"sizes": "180x180"
}
],
"start_url": "/"
}