增加回车提交事件,Terminal前端增加文件拖动解析

This commit is contained in:
OldCat 2020-12-16 13:11:34 +08:00
parent 0f765649a7
commit 1020bbb2ac
14 changed files with 596 additions and 43 deletions

View File

@ -13,7 +13,7 @@ Gin + gorm
## 更新日志
2020/12/14 修复无操作自动断开、修复网络延迟造成的js加载延迟问题
2020/12/16 前端新增文件/文件夹拖动到Terminal的自动解析功能SFTP需要修改layer弹出窗口逻辑增加回车提交事件
## 开发计划
✔ ssh功能

View File

@ -4,6 +4,7 @@ import (
"bytes"
"encoding/json"
"github.com/gorilla/websocket"
"github.com/pkg/sftp"
"golang.org/x/crypto/ssh"
"io"
"log"
@ -47,6 +48,7 @@ type SshConn struct {
// Write() be called to receive data from ssh server
ComboOutput *wsBufferWriter
Session *ssh.Session
SftpClient *sftp.Client
}
//flushComboOutput flush ssh.session combine output into websocket response
@ -96,14 +98,20 @@ func NewSshConn(cols, rows int, sshClient *ssh.Client) (*SshConn, error) {
if err := sshSession.Shell(); err != nil {
return nil, err
}
return &SshConn{StdinPipe: stdinP, ComboOutput: comboWriter, Session: sshSession}, nil
sftpclient, err := sftp.NewClient(sshClient) //创建一个sftp客户端
if err != nil {
return nil, err
}
return &SshConn{StdinPipe: stdinP, ComboOutput: comboWriter, Session: sshSession, SftpClient: sftpclient}, nil
}
func (s *SshConn) Close() {
if s.Session != nil {
s.Session.Close()
}
if s.SftpClient != nil{
s.SftpClient.Close()
}
}
//ReceiveWsMsg receive websocket msg do some handling then write into ssh.session.stdin
@ -192,7 +200,7 @@ func (ssConn *SshConn) SessionWait(quitChan chan bool) {
select {
case <-timer.C:
{
if _, err := ssConn.StdinPipe.Write([]byte{32,127}); err != nil {
if _, err := ssConn.StdinPipe.Write([]byte{32, 127}); err != nil {
log.Println("ws cmd bytes write to ssh.stdin pipe failed")
return
}

View File

@ -110,13 +110,11 @@ func WsSsh(c *gin.Context) {
}
defer client.Close()
//startTime := time.Now()
ssConn, err := core.NewSshConn(cols, rows, client)
ssConn, err := core.NewSshConn(cols, rows, client) //加入sftp客户端
if core.WshandleError(wsConn, err) {
return
}
defer ssConn.Close()
quitChan := make(chan bool, 3)
// most messages are ssh output, not webSocket input

View File

@ -54,10 +54,12 @@ editpass = function () {
obj.password = obj.desc
delete obj.setpass; //不提交用户的加密密码
console.log(obj)
parent.open_repass_index = 0;
http_send("/repass", obj)
}
add_callback = function (result) {
parent.open_add_index = 0;
parent.getinfo();
parent.layer.closeAll('iframe');
}

View File

@ -12,20 +12,25 @@ setnickname = function (data) {
username: data.Username
})
}
var open_repass_index = 0;
openpass = function (ID) {
//console.log(ID)
layer.open({
maxmin : true,
if(open_repass_index > 0){
return false
}
open_repass_index = layer.open({
type: 2,
maxmin : true,
title: '重置加密密码',
shadeClose: true,
shade: 0.4,
mask: true,
//maxmin: true, //开启最大化最小化按钮
area: ['30vw', '50vh'],
content: '/setpass?id=' + ID
content: '/setpass?id=' + ID,
shadeClose: false,
cancel: function(){
open_repass_index = 0
}
});
}
@ -142,18 +147,24 @@ search = function () {
, data: data.data.List
});
}
var open_add_index = 0;
add = function () {
layer.open({
if(open_add_index > 0){
return false
}
open_add_index = layer.open({
maxmin : true,
type: 2,
title: '添加SSH服务器',
shadeClose: true,
shade: 0.4,
mask: true,
//maxmin: true, //开启最大化最小化按钮
area: ['30vw', '50vh'],
content: '/add'
content: '/add',
shadeClose: false,
cancel: function(){
open_add_index = 0
}
});
}
@ -166,16 +177,23 @@ del_callbacl = function (result) {
getinfo()
}
open_terminal_index = 0;
open_terminal = function (ID, sername) {
layer.open({
if(open_terminal_index > 0){
return false
}
open_terminal_index = layer.open({
maxmin : true,
type: 2,
title: '打开SSH终端' + sername,
shadeClose: true,
shade: 0.4,
mask: true,
//maxmin: true, //开启最大化最小化按钮
area: ['30vw', '30vh'],
content: '/openterm?id=' + ID + "&sername=" + encodeURI(sername)
content: '/openterm?id=' + ID + "&sername=" + encodeURI(sername),
shadeClose: false,
cancel: function(){
open_terminal_index = 0
}
});
}

177
static/js/fileupload.js Normal file
View File

@ -0,0 +1,177 @@
let dropbox = document.getElementById("terms"); //要监听拖动上传的节点
let fileDrop = {
startTime: 0,
endTime: 0,
uploadLength: 0, //上传数量
//splitSize: 1024 * 1024 * 2, //文件上传分片大小
filesList: [], // 文件列表数组
errorLength: 0, //上传失败文件数量
isUpload: true, //上传状态,是否可以上传
//uploadSuspend:[], //上传暂停参数
isUploadNumber: 800,//限制单次上传数量
uploadAllSize: 0, // 上传文件总大小
uploadedSize: 0, // 已上传文件大小
topUploadedSize: 0, // 上一次文件上传大小
uploadExpectTime: 0, // 预计上传时间
//initTimer:0, // 初始化计时
speedInterval: null, //平局速度定时器
timerSpeed: 0, //速度
uploading: false,
cancel: false,
}
dropbox.addEventListener("dragleave", function (e) {
//e.stopPropagation();
e.preventDefault();
}, false);
dropbox.addEventListener("dragenter", function (e) {
//e.stopPropagation();
e.preventDefault();
}, false);
dropbox.addEventListener("dragover", function (e) {
//e.stopPropagation();
e.preventDefault();
}, false);
dropbox.addEventListener("drop", changes, false);
function changes(e) {
if(!is_login){
layer.msg("请等待服务器连接!")
}
e.preventDefault();
let items = e.dataTransfer.items, time, num = 0
if (fileDrop.uploading) {
layer.msg("已有文件队列上传中")
return false
}
if (items && items.length && items[0].webkitGetAsEntry != null) {
if (items[0].kind != 'file') return false;
}
if (fileDrop.filesList == null) fileDrop.filesList = []
for (let i = fileDrop.filesList.length - 1; i >= 0; i--) {
if (fileDrop.filesList[i].is_upload) fileDrop.filesList.splice(-i, 1)
}
function update_sync(s) {
s.getFilesAndDirectories().then(function (subFilesAndDirs) {
return iterateFilesAndDirs(subFilesAndDirs, s.path);
});
}
let iterateFilesAndDirs = function (filesAndDirs, path) {
for (let i = 0; i < filesAndDirs.length; i++) {
if (typeof (filesAndDirs[i].getFilesAndDirectories) == 'function') {
update_sync(filesAndDirs[i])
} else {
if (num > 100) {
//fileDrop.isUpload = false;
layer.msg(' '+ fileDrop.isUploadNumber +'份,无法上传,请压缩后上传!。',{icon:2,area:'405px'});
//clearTimeout(time);
return false;
}
fileDrop.filesList.push({
file: filesAndDirs[i],
path: path,
name: filesAndDirs[i].name.replace('//', '/'),
local: (path == "/" ? "" : path) + "/" + filesAndDirs[i].name.replace('//', '/'),
size: to_size(filesAndDirs[i].size),
upload: 0, //上传状态,未上传0、上传中1已上传2上传失败-1
is_upload: false
});
fileDrop.uploadAllSize += filesAndDirs[i].size
fileDrop.uploadLength++;
}
}
}
if ('getFilesAndDirectories' in e.dataTransfer) {
e.dataTransfer.getFilesAndDirectories().then(function (filesAndDirs) {
return iterateFilesAndDirs(filesAndDirs, '/');
});
}
//console.log(fileDrop.filesList)
layer.load(1, {
shade: [0.1,'#fff'] //0.1透明度的白色背景
});
setTimeout(function () {
layer.closeAll('loading')
open_upload_window()
},3000)
}
function open_upload_window() {
let template = `
<table class="layui-table" lay-even="" lay-skin="row" id="file_upload" style="table-layout: fixed;padding-top: 0">
<colgroup>
<col width="250">
<col width="150">
<col width="150">
<col>
</colgroup>
<thead>
<tr>
<th>文件路径</th>
<th>文件大小</th>
<th>状态</th>
</tr>
</thead>
<tbody align="center">
</tbody>
</table>
`
layer.open({
type: 1,
closeBtn: 1,
maxmin: true,
area: ['550px', '455px'],
btn: ['开始上传', '取消上传'],
title: '上传文件',
skin: 'file_dir_uploads',
shade: 0.4,
shadeClose: false,
content: template,
success: function () {
for (let i = 0; i < fileDrop.filesList.length; i++) {
$("#file_upload tbody").append(create_row(i, fileDrop.filesList[i]));
}
}
});
}
function create_row(index, file) {
console.log(file)
return "<tr id='" + index + "'><td title='" + file.local + "' style=\"white-space:nowrap;overflow:hidden;text-overflow: ellipsis;\">" + file.local + "</td> <td>" + file.size + "</td> <td>" + getstatu(file.upload) + "</td></tr>"
}
function getstatu(statu) {
//上传状态,未上传0、上传中1已上传2上传失败-1
if (statu == -1) {
return "<font color='red'>上传失败</font>"
} else {
if (statu == 0) {
return "<font color='black'>未上传</font>"
} else if (statu == 1) {
return "<font color='#808080'>上传中</font>"
} else {
return "<font color='green'>已上传</font>"
}
}
}
function to_size(a) {
var d = [" B", " KB", " MB", " GB", " TB", " PB"];
var e = 1024;
for (var b = 0; b < d.length; b += 1) {
if (a < e) {
var num = (b === 0 ? a : a.toFixed(2)) + d[b];
return (!isNaN((b === 0 ? a : a.toFixed(2))) && typeof num != 'undefined') ? num : '0B';
}
a /= e
}
}

View File

@ -1,3 +1,9 @@
$("#key").keydown(function(e) {
if (e.keyCode == 13) {
openterm();
return false
}
});
openterm = function () {
var pass_info = $('form').serializeArray();
var obj = {}; //声明一个对象
@ -10,6 +16,7 @@ openterm = function () {
openterm_callback = function (result) {
parent.layer.closeAll('iframe');
parent.open_terminal_index = 0;
parent.getinfo();
var getData = GetRequest();
if (top) {

324
static/js/polyfill.js Normal file
View File

@ -0,0 +1,324 @@
/**********************************
Directory Upload Proposal Polyfill
Author: Ali Alabbas (Microsoft)
**********************************/
(function() {
// Do not proceed with the polyfill if Directory interface is already natively available,
// or if webkitdirectory is not supported (i.e. not Chrome, since the polyfill only works in Chrome)
if (window.Directory || !('webkitdirectory' in document.createElement('input') && 'webkitGetAsEntry' in DataTransferItem.prototype)) {
return;
}
var allowdirsAttr = 'allowdirs',
getFilesMethod = 'getFilesAndDirectories',
isSupportedProp = 'isFilesAndDirectoriesSupported',
chooseDirMethod = 'chooseDirectory';
var separator = '/';
var Directory = function() {
this.name = '';
this.path = separator;
this._children = {};
this._items = false;
};
Directory.prototype[getFilesMethod] = function() {
var that = this;
// from drag and drop and file input drag and drop (webkitEntries)
if (this._items) {
var getItem = function(entry) {
if (entry.isDirectory) {
var dir = new Directory();
dir.name = entry.name;
dir.path = entry.fullPath;
dir._items = entry;
return dir;
} else {
return new Promise(function(resolve, reject) {
entry.file(function(file) {
resolve(file);
}, reject);
});
}
};
if (this.path === separator) {
var promises = [];
for (var i = 0; i < this._items.length; i++) {
var entry;
// from file input drag and drop (webkitEntries)
if (this._items[i].isDirectory || this._items[i].isFile) {
entry = this._items[i];
} else {
entry = this._items[i].webkitGetAsEntry();
}
promises.push(getItem(entry));
}
return Promise.all(promises);
} else {
return new Promise(function(resolve, reject) {
var dirReader = that._items.createReader();
var promises = [];
var readEntries = function() {
dirReader.readEntries(function(entries) {
if (!entries.length) {
resolve(Promise.all(promises));
} else {
for (var i = 0; i < entries.length; i++) {
promises.push(getItem(entries[i]));
}
readEntries();
}
}, reject);
};
readEntries();
});
}
// from file input manual selection
} else {
var arr = [];
for (var child in this._children) {
arr.push(this._children[child]);
}
return Promise.resolve(arr);
}
};
// set blank as default for all inputs
HTMLInputElement.prototype[getFilesMethod] = function() {
return Promise.resolve([]);
};
// if OS is Mac, the combined directory and file picker is supported
HTMLInputElement.prototype[isSupportedProp] = navigator.appVersion.indexOf("Mac") !== -1;
HTMLInputElement.prototype[allowdirsAttr] = undefined;
HTMLInputElement.prototype[chooseDirMethod] = undefined;
// expose Directory interface to window
window.Directory = Directory;
/********************
**** File Input ****
********************/
var convertInputs = function(nodes) {
var recurse = function(dir, path, fullPath, file) {
var pathPieces = path.split(separator);
var dirName = pathPieces.shift();
if (pathPieces.length > 0) {
var subDir = new Directory();
subDir.name = dirName;
subDir.path = separator + fullPath;
if (!dir._children[subDir.name]) {
dir._children[subDir.name] = subDir;
}
recurse(dir._children[subDir.name], pathPieces.join(separator), fullPath, file);
} else {
dir._children[file.name] = file;
}
};
for (var i = 0; i < nodes.length; i++) {
var node = nodes[i];
if (node.tagName === 'INPUT' && node.type === 'file') {
var getFiles = function() {
var files = node.files;
if (draggedAndDropped) {
files = node.webkitEntries;
draggedAndDropped = false;
} else {
if (files.length === 0) {
files = node.shadowRoot.querySelector('#input1').files;
if (files.length === 0) {
files = node.shadowRoot.querySelector('#input2').files;
if (files.length === 0) {
files = node.webkitEntries;
}
}
}
}
return files;
};
var draggedAndDropped = false;
node.addEventListener('drop', function(e) {
draggedAndDropped = true;
}, false);
if (node.hasAttribute(allowdirsAttr)) {
// force multiple selection for default behavior
if (!node.hasAttribute('multiple')) {
node.setAttribute('multiple', '');
}
var shadow = node.createShadowRoot();
node[chooseDirMethod] = function() {
// can't do this without an actual click
console.log('This is unsupported. For security reasons the dialog cannot be triggered unless it is a response to some user triggered event such as a click on some other element.');
};
shadow.innerHTML = '<div style="border: 1px solid #999; padding: 3px; width: 235px; box-sizing: content-box; font-size: 14px; height: 21px;">'
+ '<div id="fileButtons" style="box-sizing: content-box;">'
+ '<button id="button1" style="width: 100px; box-sizing: content-box;">Choose file(s)...</button>'
+ '<button id="button2" style="width: 100px; box-sizing: content-box; margin-left: 3px;">Choose folder...</button>'
+ '</div>'
+ '<div id="filesChosen" style="padding: 3px; display: none; box-sizing: content-box;"><span id="filesChosenText">files selected...</span>'
+ '<a id="clear" title="Clear selection" href="javascript:;" style="text-decoration: none; float: right; margin: -3px -1px 0 0; padding: 3px; font-weight: bold; font-size: 16px; color:#999; box-sizing: content-box;">&times;</a>'
+ '</div>'
+ '</div>'
+ '<input id="input1" type="file" multiple style="display: none;">'
+ '<input id="input2" type="file" webkitdirectory style="display: none;">'
+ '</div>';
shadow.querySelector('#button1').onclick = function(e) {
e.preventDefault();
shadow.querySelector('#input1').click();
};
shadow.querySelector('#button2').onclick = function(e) {
e.preventDefault();
shadow.querySelector('#input2').click();
};
var toggleView = function(defaultView, filesLength) {
shadow.querySelector('#fileButtons').style.display = defaultView ? 'block' : 'none';
shadow.querySelector('#filesChosen').style.display = defaultView ? 'none' : 'block';
if (!defaultView) {
shadow.querySelector('#filesChosenText').innerText = filesLength + ' file' + (filesLength > 1 ? 's' : '') + ' selected...';
}
};
var changeHandler = function(e) {
node.dispatchEvent(new Event('change'));
toggleView(false, getFiles().length);
};
shadow.querySelector('#input1').onchange = shadow.querySelector('#input2').onchange = changeHandler;
var clear = function (e) {
toggleView(true);
var form = document.createElement('form');
node.parentNode.insertBefore(form, node);
node.parentNode.removeChild(node);
form.appendChild(node);
form.reset();
form.parentNode.insertBefore(node, form);
form.parentNode.removeChild(form);
// reset does not instantly occur, need to give it some time
setTimeout(function() {
node.dispatchEvent(new Event('change'));
}, 1);
};
shadow.querySelector('#clear').onclick = clear;
}
node.addEventListener('change', function() {
var dir = new Directory();
var files = getFiles();
if (files.length > 0) {
if (node.hasAttribute(allowdirsAttr)) {
toggleView(false, files.length);
}
// from file input drag and drop (webkitEntries)
if (files[0].isFile || files[0].isDirectory) {
dir._items = files;
} else {
for (var j = 0; j < files.length; j++) {
var file = files[j];
var path = file.webkitRelativePath;
var fullPath = path.substring(0, path.lastIndexOf(separator));
recurse(dir, path, fullPath, file);
}
}
} else if (node.hasAttribute(allowdirsAttr)) {
toggleView(true, files.length);
}
this[getFilesMethod] = function() {
return dir[getFilesMethod]();
};
});
}
}
};
// polyfill file inputs when the DOM loads
document.addEventListener('DOMContentLoaded', function(event) {
convertInputs(document.getElementsByTagName('input'));
});
// polyfill file inputs that are created dynamically and inserted into the body
var observer = new MutationObserver(function(mutations, observer) {
for (var i = 0; i < mutations.length; i++) {
if (mutations[i].addedNodes.length > 0) {
convertInputs(mutations[i].addedNodes);
}
}
});
observer.observe(document.body, {childList: true, subtree: true});
/***********************
**** Drag and drop ****
***********************/
// keep a reference to the original method
var _addEventListener = EventTarget.prototype.addEventListener;
DataTransfer.prototype[getFilesMethod] = function() {
return Promise.resolve([]);
};
EventTarget.prototype.addEventListener = function(type, listener, useCapture) {
if (type === 'drop') {
var _listener = listener;
listener = function(e) {
var dir = new Directory();
dir._items = e.dataTransfer.items;
e.dataTransfer[getFilesMethod] = function() {
return dir[getFilesMethod]();
};
_listener(e);
};
}
// call the original method
return _addEventListener.apply(this, arguments);
};
}());

View File

@ -1,33 +1,33 @@
var protocol = document.location.protocol.split(':')[0];
var is_login = false;
const protocol = document.location.protocol.split(':')[0];
var ws_p = "ws";
if (protocol == "https") {
ws_p = "wss";
}
var socket = new WebSocket(ws_p + '://' + window.location.host + '/v1/term/' + GetQueryString("sid"));
var term = new Terminal({cols: 180, rows: 50, screenKeys: true, cursorBlink: true, cursorStyle: "block"});
const token = window.localStorage.getItem("token")
if (token == "") {
if (window != top) {
top.location.href = "/login";
}
window.location.href = "/login";
}
const auth = {
type: "auth",
token: token,
}
const socket = new WebSocket(ws_p + '://' + window.location.host + '/v1/term/' + GetQueryString("sid"));
const term = new Terminal({cols: 180, rows: 50, screenKeys: true, cursorBlink: true, cursorStyle: "block"});
term.open(document.getElementById('terms'));
window.onresize = function () {
fit.fit(term);
};
socket.onopen = function () {
var token = window.localStorage.getItem("token")
if (token == "") {
if (window != top) {
top.location.href = "/login";
}
window.location.href = "/login";
return
}
var auth = {
type: "auth",
token: token,
}
socket.send(JSON.stringify(auth)); //验证权限
term.write("正在验证\r\n");
term.toggleFullscreen(true);
fit.fit(term);
term.on('data', function (data) {
var sdata = {
let sdata = {
type: "cmd",
cmd: data,
}
@ -36,7 +36,7 @@ socket.onopen = function () {
term.on('resize', size => {
//console.log('resize', [size.cols, size.rows]);
var sdata = {
let sdata = {
type: "resize",
cols: size.cols,
rows: size.rows,
@ -45,13 +45,18 @@ socket.onopen = function () {
});
socket.onmessage = function (msg) {
if(!is_login){
is_login = true
}
term.write(msg.data);
};
socket.onerror = function (e) {
is_login = false
console.log(e);
};
socket.onclose = function (e) {
is_login = false
console.log(e);
term.write("连接已断开:" + e.reason + "\r\n");
//term.destroy();

View File

@ -75,7 +75,7 @@
<script src="https://cdn.bootcdn.net/ajax/libs/blueimp-md5/2.18.0/js/md5.min.js"></script>
<script src="/static/js/net.js"></script>
<script src="/static/js/aes.js?v=1.0"></script>
<script src="/static/js/add.js?v=1.93"></script>
<script src="/static/js/add.js?v=1.95"></script>
<script>
var index = layer.load(0, {shade: [0.1,'#fff']});
document.onreadystatechange = completeLoading;
@ -86,6 +86,11 @@
layer.close(index);
}
}
$(document).keyup(function(event){
if(event.keyCode ==13){
addser();
}
});
</script>
</body>
</html>

View File

@ -38,7 +38,7 @@
<script src="https://cdn.bootcdn.net/ajax/libs/crypto-js/4.0.0/aes.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/blueimp-md5/2.18.0/js/md5.min.js"></script>
<script src="/static/js/net.js?v=0.1"></script>
<script src="/static/js/openterm.js?v=0.91"></script>
<script src="/static/js/openterm.js?v=0.99.1"></script>
<script>
var index = layer.load(0, {shade: [0.1,'#fff']});
document.onreadystatechange = completeLoading;

View File

@ -51,7 +51,7 @@
<script src="https://cdn.bootcdn.net/ajax/libs/blueimp-md5/2.18.0/js/md5.min.js"></script>
<script src="/static/js/net.js?v=1.0"></script>
<script src="/static/js/aes.js?v=1.6"></script>
<script src="/static/js/add.js?v=1.95"></script>
<script src="/static/js/add.js?v=1.96"></script>
<script>
var index = layer.load(0, {shade: [0.1,'#fff']});
document.onreadystatechange = completeLoading;
@ -62,6 +62,11 @@
layer.close(index);
}
}
$(document).keyup(function(event){
if(event.keyCode ==13){
editpass();
}
});
</script>
</body>
</html>

View File

@ -44,7 +44,7 @@
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<script src="/static/assets/layui.all.js?v=1"></script>
<script src="/static/js/net.js?v=1.3"></script>
<script src="/static/js/console.js?v=1.93"></script>
<script src="/static/js/console.js?v=1.99.1"></script>
<script>
var index = layer.load(0, {shade: [0.1,'#fff']});
document.onreadystatechange = completeLoading;

View File

@ -6,6 +6,7 @@
<link href="https://cdn.bootcdn.net/ajax/libs/xterm/3.9.2/xterm.min.css" rel="stylesheet" type="text/css"/>
<link href="/static/assets/css/layui.css" rel="stylesheet" type="text/css"/>
<link href="/static/css/fullscreen.min.css" rel="stylesheet" type="text/css"/>
<style>
html{
@ -27,14 +28,17 @@
</style>
</head>
<body>
<div id="terms" style="height: 100vh;width: 100vw"></div>
</body>
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/layer/3.1.1/layer.min.js"></script>
<script src="/static/assets/layui.all.js?v=1"></script>
<script src="/static/xterm/xterm.min.js"></script>
<script src="/static/xterm/fullscreen.min.js"></script>
<script src="/static/xterm/fit.min.js"></script>
<script src="/static/xterm/search.min.js"></script>
<script src="/static/js/net.js?v=1.9"></script>
<script src="/static/xterm/main.js?v=0.6"></script>
<script src="/static/xterm/main.js?v=0.62"></script>
<script src="/static/js/polyfill.js"></script>
<script src="/static/js/fileupload.js?v=0.30"></script>
</html>