diff --git a/README.md b/README.md index 9a9e8d9..6182067 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,8 @@ go版本多用户webssh管理工具 ssh2ws部分代码修改自https://github.com/hequan2017/go-webssh +2020/12/17 新增WEB_SFTP功能,拖动文件到终端窗口里即可上传 + 服务端不保存用户明文密码,且不保存解密秘钥,如需对其他用户开放,请不要修改此部分代码,以免造成不必要的损失! @@ -14,10 +16,11 @@ Gin + gorm ## 更新日志 2020/12/14 修复无操作自动断开、修复网络延迟造成的js加载延迟问题 2020/12/16 前端新增文件/文件夹拖动到Terminal的自动解析功能(SFTP需要),修改layer弹出窗口逻辑,增加回车提交事件 + 2020/12/17 增加在线sftp文件上传功能 * ## 开发计划 ✔ ssh功能 -× 服务器文件管理 +✔ sftp文件上传功能 ## 在线演示 [点击进入SSH云管理平台](https://www.do18.cn) diff --git a/common/core/ssh_shell_conn.go b/common/core/ssh_shell_conn.go index 4f5f009..21553fb 100644 --- a/common/core/ssh_shell_conn.go +++ b/common/core/ssh_shell_conn.go @@ -194,7 +194,7 @@ func (ssConn *SshConn) SessionWait(quitChan chan bool) { // log.Println("ssh session wait failed") // setQuit(quitChan) //} - timer := time.NewTicker(time.Second * 3) + timer := time.NewTicker(time.Second * 5) defer timer.Stop() for { select { diff --git a/common/sftp_clients/sftp.go b/common/sftp_clients/sftp.go new file mode 100644 index 0000000..3c774b9 --- /dev/null +++ b/common/sftp_clients/sftp.go @@ -0,0 +1,181 @@ +package sftp_clients + +import ( + "encoding/base64" + "encoding/json" + "github.com/gorilla/websocket" + "github.com/pkg/sftp" + "log" + "path" + "sync" + "time" +) + +const ( + getpwd = "getpwd" + upload = "upload" +) + +type sftp_req struct { + Type string `json:"type"` + FilePath string `json:"filepath"` + FileName string `json:"filename"` + FileData string `json:"filedata"` +} + +type sftp_resp struct { + Code int `json:"code"` + Type string `json:"type"` + Msg string `json:"msg"` + Data string `json:"data"` +} + +type MyClient struct { + Uid uint + Sftp *sftp.Client +} + +type clients struct { + sync.RWMutex + C map[string]*MyClient +} + +var Client clients + +func init() { + Client = clients{C: make(map[string]*MyClient, 1000)} +} + +func (c *MyClient) ReceiveWsMsg(wsConn *websocket.Conn, exitCh chan bool) { + defer setQuit(exitCh) + go c.SessionWait(wsConn, exitCh) + for { + select { + case <-exitCh: + return + default: + _, wsData, err := wsConn.ReadMessage() + if err != nil { + log.Println(err.Error()) + //logrus.WithError(err).Error("reading webSocket message failed") + return + } + //unmashal bytes into struct + msgObj := sftp_req{} + if err := json.Unmarshal(wsData, &msgObj); err != nil { + log.Println("unmarshal websocket message failed:", string(wsData)) + continue + } + msgresp := sftp_resp{} + switch msgObj.Type { + case getpwd: + msgresp.Code = 200 + msgresp.Type = "pwd" + path, err := c.Sftp.Getwd() + if err != nil { + msgresp.Code = 404 + msgresp.Msg = "服务器Path获取失败" + msgresp.Data = err.Error() + log.Println("sftp getpwd err:", err.Error()) + } + msgresp.Data = path + msg, _ := json.Marshal(msgresp) + if err := wsConn.WriteMessage(websocket.TextMessage, msg); err != nil { + log.Println("sftp client getpwd err:", err.Error()) + return + } + case upload: + msgresp.Code = 200 + msgresp.Type = "upload" + io_data, err := base64.StdEncoding.DecodeString(msgObj.FileData) + if err != nil { + msgresp.Code = 401 + msgresp.Msg = "文件解析失败" + msgresp.Data = err.Error() + log.Println("sftp base64decode err:", err.Error()) + msg, _ := json.Marshal(msgresp) + if err := wsConn.WriteMessage(websocket.TextMessage, msg); err != nil { + log.Println("sftp base64decode send err:", err.Error()) + return + } + } + + if err := c.Sftp.MkdirAll(msgObj.FilePath); err != nil { + msgresp.Code = 402 + msgresp.Msg = "服务器创建目录失败" + msgresp.Data = err.Error() + log.Println("sftp mkdir err:", err.Error()) + msg, _ := json.Marshal(msgresp) + if err := wsConn.WriteMessage(websocket.TextMessage, msg); err != nil { + log.Println("sftp mkdir send err:", err.Error()) + return + } + continue + } + + file, err := c.Sftp.Create(path.Join(msgObj.FilePath, msgObj.FileName)) + if err != nil { + msgresp.Code = 403 + msgresp.Msg = "服务器文件创建失败" + msgresp.Data = err.Error() + log.Println("sftp create file err:", err.Error()) + msg, _ := json.Marshal(msgresp) + if err := wsConn.WriteMessage(websocket.TextMessage, msg); err != nil { + log.Println("sftp create file send err:", err.Error()) + return + } + continue + } + + defer file.Close() + + if _, err := file.Write(io_data); err != nil { + msgresp.Code = 405 + msgresp.Msg = "服务器文件写入失败" + msgresp.Data = err.Error() + log.Println("sftp write file err:", err.Error()) + msg, _ := json.Marshal(msgresp) + if err := wsConn.WriteMessage(websocket.TextMessage, msg); err != nil { + log.Println("sftp write file send err:", err.Error()) + return + } + continue + } + file.Close() + filepath := path.Join(msgObj.FilePath, msgObj.FileName) + msgresp.Code = 200 + msgresp.Msg = "OK" + msgresp.Data = filepath + //log.Println("file write ok") + msg, _ := json.Marshal(msgresp) + if err := wsConn.WriteMessage(websocket.TextMessage, msg); err != nil { + log.Println("sftp write file send err:", err.Error()) + return + } + } + } + } +} + +func (c *MyClient) SessionWait(wsConn *websocket.Conn, quitChan chan bool) { + timer := time.NewTicker(time.Second * 30) + defer timer.Stop() + defer setQuit(quitChan) + for { + select { + case <-timer.C: + { + if err := wsConn.WriteMessage(websocket.TextMessage, []byte("pong")); err != nil { + log.Println("sftp pong send err :", err.Error()) + return + } + } + case <-quitChan: + return + } + } +} + +func setQuit(ch chan bool) { + ch <- true +} diff --git a/controller/stfp.go b/controller/stfp.go new file mode 100644 index 0000000..bba47ac --- /dev/null +++ b/controller/stfp.go @@ -0,0 +1,114 @@ +package controller + +import ( + "encoding/json" + "fmt" + "github.com/gin-gonic/gin" + "github.com/gorilla/websocket" + "log" + "ssh_manage/common" + "ssh_manage/common/core" + "ssh_manage/common/sftp_clients" + "ssh_manage/errcode" + "ssh_manage/model/Apiform" +) + +type sftp_req struct { + Type string `json:"type"` + Token string `json:"token"` +} + +type sftp_resp struct { + Code int `json:"code"` + Type string `json:"type"` + Msg string `json:"msg"` + Data string `json:"data"` +} + +func Sftp_ssh(c *gin.Context) { + wsConn, err := upGrader.Upgrade(c.Writer, c.Request, nil) + if core.HandleError(c, err) { + return + } + defer wsConn.Close() + + var auth Apiform.WsAuth + + if c.ShouldBindUri(&auth) != nil { + wsConn.WriteMessage(websocket.TextMessage, []byte("参数错误\r\n")) + wsConn.Close() + return + } + + for { + _, wsData, err := wsConn.ReadMessage() + if err != nil { + log.Println(err.Error()) + wsConn.Close() + //logrus.WithError(err).Error("reading webSocket message failed") + return + } + //unmashal bytes into struct + msgObj := sftp_req{} + if err := json.Unmarshal(wsData, &msgObj); err != nil { + log.Println("Auth : unmarshal websocket message failed:", string(wsData)) + continue + } + resp_msg := sftp_resp{} + token := msgObj.Token + claims, err := common.ParseToken(token) + valid := claims.Valid() + if valid != nil || err != nil { + resp_msg.Code = errcode.S_auth_fmt_err + resp_msg.Msg = "身份令牌校验不通过" + resp_msg.Data = err.Error() + msg, _ := json.Marshal(resp_msg) + if err := wsConn.WriteMessage(websocket.TextMessage, msg); err != nil { + log.Println("sftp token fmt err:", err) + } + wsConn.Close() + return + } + if claims.Userid != sftp_clients.Client.C[auth.Sid].Uid { //身份与缓存不符合 + resp_msg.Code = errcode.S_auth_fmt_err + resp_msg.Msg = "用户权限不通过" + resp_msg.Data = err.Error() + msg, _ := json.Marshal(resp_msg) + if err := wsConn.WriteMessage(websocket.TextMessage, msg); err != nil { + log.Println("sftp server_user err:", err) + } + wsConn.Close() + return + } + + path, err := sftp_clients.Client.C[auth.Sid].Sftp.Getwd() + if err != nil { + resp_msg.Code = errcode.S_send_err + resp_msg.Type = "connect" + resp_msg.Msg = "SFTP连接失败" + msg, _ := json.Marshal(resp_msg) + if err := wsConn.WriteMessage(websocket.TextMessage, msg); err != nil { + log.Println("sftp connect err:", err) + } + return + } + + resp_msg.Code = 200 + resp_msg.Type = "connect" + resp_msg.Msg = "连接成功" + resp_msg.Data = path + msg, _ := json.Marshal(resp_msg) + if err := wsConn.WriteMessage(websocket.TextMessage, msg); err != nil { + log.Println("sftp return err:", err) + return + } + + break + //break + } + quitChan := make(chan bool, 2) + go sftp_clients.Client.C[auth.Sid].ReceiveWsMsg(wsConn, quitChan) + <-quitChan //任意协程退出则结束 + fmt.Println("Sftp Exit") + log.Println("sftp websocket finished") +} diff --git a/controller/term.go b/controller/term.go index e6f555d..501d970 100644 --- a/controller/term.go +++ b/controller/term.go @@ -10,6 +10,7 @@ import ( "net/http" "ssh_manage/common" "ssh_manage/common/core" + "ssh_manage/common/sftp_clients" "ssh_manage/database" "ssh_manage/model/Apiform" "strconv" @@ -52,8 +53,8 @@ func WsSsh(c *gin.Context) { var ser_info Apiform.SerInfo //接收反序列化数据 var auth Apiform.WsAuth - if c.ShouldBindUri(&auth) != nil{ - wsConn.WriteMessage(websocket.TextMessage,[]byte("参数错误\r\n")) + if c.ShouldBindUri(&auth) != nil { + wsConn.WriteMessage(websocket.TextMessage, []byte("参数错误\r\n")) wsConn.Close() return } @@ -83,20 +84,20 @@ func WsSsh(c *gin.Context) { cache := database.Cache.Get() defer cache.Close() //log.Println(auth) - s_info,err := redis.Bytes(cache.Do("GET", auth.Sid)) + s_info, err := redis.Bytes(cache.Do("GET", auth.Sid)) //log.Println(string(s_info)) - if err != nil || len(s_info) == 0{ + if err != nil || len(s_info) == 0 { wsConn.WriteMessage(websocket.TextMessage, []byte("连接超时,请重试!\r\n")) wsConn.Close() return } - if json.Unmarshal(s_info,&ser_info) != nil{ + if json.Unmarshal(s_info, &ser_info) != nil { wsConn.WriteMessage(websocket.TextMessage, []byte("服务器信息获取失败,请重试!\r\n")) wsConn.Close() return } //log.Println(ser_info) - if claims.Userid != ser_info.BindUser{ //验证权限 + if claims.Userid != ser_info.BindUser { //验证权限 wsConn.WriteMessage(websocket.TextMessage, []byte("权限验证失败,请重试!\r\n")) wsConn.Close() return @@ -110,10 +111,18 @@ func WsSsh(c *gin.Context) { } defer client.Close() //startTime := time.Now() - ssConn, err := core.NewSshConn(cols, rows, client) //加入sftp客户端 + ssConn, err := core.NewSshConn(cols, rows, client) //加入sftp客户端 if core.WshandleError(wsConn, err) { return } + sftp_clients.Client.Lock() + sftp_clients.Client.C[auth.Sid] = &sftp_clients.MyClient{ser_info.BindUser, ssConn.SftpClient} + sftp_clients.Client.Unlock() + defer func() { + sftp_clients.Client.Lock() + delete(sftp_clients.Client.C, auth.Sid) //释放SFTP客户端 + sftp_clients.Client.Unlock() + }() defer ssConn.Close() quitChan := make(chan bool, 3) diff --git a/serve.go b/serve.go index eeb6fc3..85bc02b 100644 --- a/serve.go +++ b/serve.go @@ -55,6 +55,7 @@ func main() { api.POST("/login", controller.Login) api.POST("/send", controller.Send) api.GET("/term/:sid", controller.WsSsh) + api.GET("/sftp/:sid", controller.Sftp_ssh) api.Use(middleware.Auth()).GET("/userinfo", controller.Info) api.Use(middleware.Auth()).POST("/nickname", controller.UpdataNick) api.Use(middleware.Auth()).POST("/addser", controller.Addser) diff --git a/static/js/console.js b/static/js/console.js index 31f9b83..a550cc4 100644 --- a/static/js/console.js +++ b/static/js/console.js @@ -65,7 +65,7 @@ getinfo = function () { } , parseData: function (res) { //将原始数据解析成 table 组件所规定的数据 if (res.token) { - window.localStorage.setItem("token", result.token) //更新token + window.localStorage.setItem("token", res.token) //更新token } if (res.code == 301 || res.code == 302) { //Token校验失败或过期 layer.msg(res.msg, function () { diff --git a/static/js/fileupload.js b/static/js/fileupload.js index 525c17b..4f48616 100644 --- a/static/js/fileupload.js +++ b/static/js/fileupload.js @@ -8,8 +8,8 @@ let fileDrop = { filesList: [], // 文件列表数组 errorLength: 0, //上传失败文件数量 isUpload: true, //上传状态,是否可以上传 - //uploadSuspend:[], //上传暂停参数 - isUploadNumber: 800,//限制单次上传数量 + uploadSuspend: false, //上传暂停参数 + isUploadNumber: 150,//限制单次上传数量 uploadAllSize: 0, // 上传文件总大小 uploadedSize: 0, // 已上传文件大小 topUploadedSize: 0, // 上一次文件上传大小 @@ -19,6 +19,30 @@ let fileDrop = { timerSpeed: 0, //速度 uploading: false, cancel: false, + done: false, +} + +function file_init() { + 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) { @@ -39,8 +63,9 @@ dropbox.addEventListener("dragover", function (e) { dropbox.addEventListener("drop", changes, false); function changes(e) { - if(!is_login){ + if (!sftp_ready) { layer.msg("请等待服务器连接!") + return false } e.preventDefault(); let items = e.dataTransfer.items, time, num = 0 @@ -67,10 +92,11 @@ function changes(e) { if (typeof (filesAndDirs[i].getFilesAndDirectories) == 'function') { update_sync(filesAndDirs[i]) } else { - if (num > 100) { + if (num > fileDrop.isUploadNumber) { //fileDrop.isUpload = false; - layer.msg(' '+ fileDrop.isUploadNumber +'份,无法上传,请压缩后上传!。',{icon:2,area:'405px'}); + layer.msg(' ' + fileDrop.isUploadNumber + '份,无法上传,请压缩后上传!。', {icon: 2, area: '405px'}); //clearTimeout(time); + file_init() return false; } fileDrop.filesList.push({ @@ -94,30 +120,34 @@ function changes(e) { } //console.log(fileDrop.filesList) layer.load(1, { - shade: [0.1,'#fff'] //0.1透明度的白色背景 + shade: [0.1, '#fff'] //0.1透明度的白色背景 }); + getpwd(getpwd_callback) //采用回调函数的方式检查SFTP服务是否可用 +} + +function getpwd_callback(msg) { + server_pwd = msg.data setTimeout(function () { layer.closeAll('loading') open_upload_window() - },3000) - + }, 1500) } function open_upload_window() { - + //console.log(fileDrop.filesList[0]) let template = `
| 文件路径 | -文件大小 | -状态 | +文件路径(共` + fileDrop.uploadLength + `个文件) | +文件大小(共` + to_size(fileDrop.uploadAllSize) + `) | +文件状态 |
|---|---|---|---|---|---|
| " + file.size + " | " + getstatu(file.upload) + " | ||||
| " + file.size + " | " + getstatu(file.upload) + " |