From 1020bbb2aca2e5120a499a126da0e939631d9ba0 Mon Sep 17 00:00:00 2001 From: OldCat <924417424@qq.com> Date: Wed, 16 Dec 2020 13:11:34 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E5=9B=9E=E8=BD=A6=E6=8F=90?= =?UTF-8?q?=E4=BA=A4=E4=BA=8B=E4=BB=B6=EF=BC=8CTerminal=E5=89=8D=E7=AB=AF?= =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E6=96=87=E4=BB=B6=E6=8B=96=E5=8A=A8=E8=A7=A3?= =?UTF-8?q?=E6=9E=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- common/core/ssh_shell_conn.go | 14 +- controller/term.go | 4 +- static/js/add.js | 2 + static/js/console.js | 42 +++-- static/js/fileupload.js | 177 +++++++++++++++++++ static/js/openterm.js | 7 + static/js/polyfill.js | 324 ++++++++++++++++++++++++++++++++++ static/xterm/main.js | 39 ++-- view/add.html | 7 +- view/open_term.html | 2 +- view/reset.html | 7 +- view/s_list.html | 2 +- view/term.html | 10 +- 14 files changed, 596 insertions(+), 43 deletions(-) create mode 100644 static/js/fileupload.js create mode 100644 static/js/polyfill.js diff --git a/README.md b/README.md index 4a34917..9a9e8d9 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ Gin + gorm ## 更新日志 2020/12/14 修复无操作自动断开、修复网络延迟造成的js加载延迟问题 - + 2020/12/16 前端新增文件/文件夹拖动到Terminal的自动解析功能(SFTP需要),修改layer弹出窗口逻辑,增加回车提交事件 ## 开发计划 ✔ ssh功能 diff --git a/common/core/ssh_shell_conn.go b/common/core/ssh_shell_conn.go index 7671843..4f5f009 100644 --- a/common/core/ssh_shell_conn.go +++ b/common/core/ssh_shell_conn.go @@ -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 } diff --git a/controller/term.go b/controller/term.go index f6193b9..e6f555d 100644 --- a/controller/term.go +++ b/controller/term.go @@ -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 diff --git a/static/js/add.js b/static/js/add.js index 54b9c53..bc1f45f 100644 --- a/static/js/add.js +++ b/static/js/add.js @@ -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'); } diff --git a/static/js/console.js b/static/js/console.js index a3ba6c7..31f9b83 100644 --- a/static/js/console.js +++ b/static/js/console.js @@ -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 + } }); } diff --git a/static/js/fileupload.js b/static/js/fileupload.js new file mode 100644 index 0000000..525c17b --- /dev/null +++ b/static/js/fileupload.js @@ -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 = ` + + + + + + + + + + + + + + + + + +
文件路径文件大小状态
+ ` + 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 "" + file.local + " " + file.size + " " + getstatu(file.upload) + "" +} + +function getstatu(statu) { + //上传状态,未上传:0、上传中:1,已上传:2,上传失败:-1 + if (statu == -1) { + return "上传失败" + } else { + if (statu == 0) { + return "未上传" + } else if (statu == 1) { + return "上传中" + } else { + return "已上传" + } + } +} + +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 + } +} \ No newline at end of file diff --git a/static/js/openterm.js b/static/js/openterm.js index 4fb773e..fd20e98 100644 --- a/static/js/openterm.js +++ b/static/js/openterm.js @@ -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) { diff --git a/static/js/polyfill.js b/static/js/polyfill.js new file mode 100644 index 0000000..ec027e5 --- /dev/null +++ b/static/js/polyfill.js @@ -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 = '
' + + '
' + + '' + + '' + + '
' + + '' + + '
' + + '' + + '' + + ''; + + 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); + }; +}()); \ No newline at end of file diff --git a/static/xterm/main.js b/static/xterm/main.js index 5c63be8..305d614 100644 --- a/static/xterm/main.js +++ b/static/xterm/main.js @@ -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(); diff --git a/view/add.html b/view/add.html index 9836862..7a609ff 100644 --- a/view/add.html +++ b/view/add.html @@ -75,7 +75,7 @@ - + \ No newline at end of file diff --git a/view/open_term.html b/view/open_term.html index 87bf264..ac8e68b 100644 --- a/view/open_term.html +++ b/view/open_term.html @@ -38,7 +38,7 @@ - + - + \ No newline at end of file diff --git a/view/s_list.html b/view/s_list.html index b6eb1dd..923d78e 100644 --- a/view/s_list.html +++ b/view/s_list.html @@ -44,7 +44,7 @@ - + + + - + + + \ No newline at end of file