From bc9daf2b01ffd6236925d92d0f7069b853f0a64b Mon Sep 17 00:00:00 2001 From: dushixiang <798148596@qq.com> Date: Fri, 8 Jan 2021 23:01:41 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B9docker=E9=BB=98=E8=AE=A4?= =?UTF-8?q?=E6=97=B6=E5=8C=BA=E4=B8=BA=E4=B8=8A=E6=B5=B7=20=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E4=BA=86=E8=AE=B0=E4=BD=8F=E7=99=BB=E5=BD=95=E6=97=A0?= =?UTF-8?q?=E6=95=88=E7=9A=84=E9=97=AE=E9=A2=98=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E4=BA=86ssh=E4=B8=8B=E8=BD=BD=E6=96=87=E4=BB=B6=E5=90=8D?= =?UTF-8?q?=E7=A7=B0=E4=B8=8D=E6=AD=A3=E7=A1=AE=E7=9A=84=E9=97=AE=E9=A2=98?= =?UTF-8?q?=20=E6=8E=88=E6=9D=83=E5=87=AD=E8=AF=81=E5=A2=9E=E5=8A=A0?= =?UTF-8?q?=E4=BA=86=E5=AF=86=E9=92=A5=E7=B1=BB=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 2 + README.md | 2 + pkg/api/account.go | 26 +++++-- pkg/api/asset.go | 5 +- pkg/api/credential.go | 50 +++++++++++++ pkg/api/middleware.go | 11 ++- pkg/api/routes.go | 2 +- pkg/api/session.go | 17 +++-- pkg/api/ssh.go | 49 +++++++++--- pkg/api/tunnel.go | 8 +- pkg/guacd/guacd.go | 4 +- pkg/handle/runner.go | 2 +- pkg/model/asset.go | 5 ++ pkg/model/credential.go | 19 +++-- web/package.json | 2 +- web/src/common/constants.js | 4 +- web/src/components/Login.js | 7 -- web/src/components/access/Access.js | 8 +- web/src/components/asset/Asset.js | 2 +- web/src/components/asset/AssetModal.js | 10 +-- web/src/components/credential/Credential.js | 39 +++++++--- .../components/credential/CredentialModal.js | 74 +++++++++++++++++-- 22 files changed, 274 insertions(+), 74 deletions(-) diff --git a/Dockerfile b/Dockerfile index d76895a..e3423be 100644 --- a/Dockerfile +++ b/Dockerfile @@ -25,6 +25,8 @@ ENV DB sqlite ENV SQLITE_FILE 'next-terminal.db' ENV SERVER_PORT 8088 ENV SERVER_ADDR 0.0.0.0:$SERVER_PORT +ENV TIME_ZONE=Asia/Shanghai +RUN ln -snf /usr/share/zoneinfo/$TIME_ZONE /etc/localtime && echo $TIME_ZONE > /etc/timezone WORKDIR /usr/local/next-terminal diff --git a/README.md b/README.md index 19d7072..bd17cef 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ # Next Terminal 你的下一个终端。 +![Docker image](https://github.com/dushixiang/next-terminal/workflows/Docker%20image/badge.svg?branch=master) + ## 快速了解 Next Terminal是使用Golang和React开发的一款HTML5的远程桌面网关,具有小巧、易安装、易使用、资源占用小的特点,支持RDP、SSH、VNC和Telnet协议的连接和管理。 diff --git a/pkg/api/account.go b/pkg/api/account.go index cda28e5..ae898c3 100644 --- a/pkg/api/account.go +++ b/pkg/api/account.go @@ -1,13 +1,11 @@ package api import ( - "log" - "time" - "next-terminal/pkg/global" "next-terminal/pkg/model" "next-terminal/pkg/totp" "next-terminal/pkg/utils" + "time" "github.com/labstack/echo/v4" ) @@ -15,6 +13,7 @@ import ( type LoginAccount struct { Username string `json:"username"` Password string `json:"password"` + Remember bool `json:"remember"` TOTP string `json:"totp"` } @@ -28,6 +27,12 @@ type ChangePassword struct { OldPassword string `json:"oldPassword"` } +type Authorization struct { + Token string + Remember bool + User model.User +} + func LoginEndpoint(c echo.Context) error { var loginAccount LoginAccount if err := c.Bind(&loginAccount); err != nil { @@ -43,15 +48,24 @@ func LoginEndpoint(c echo.Context) error { return Fail(c, -1, "您输入的账号或密码不正确") } - log.Println(user, loginAccount) - if !totp.Validate(loginAccount.TOTP, user.TOTPSecret) { return Fail(c, -2, "您的TOTP不匹配") } token := utils.UUID() - global.Cache.Set(token, user, time.Minute*time.Duration(30)) + authorization := Authorization{ + Token: token, + Remember: loginAccount.Remember, + User: user, + } + + if authorization.Remember { + // 记住登录有效期两周 + global.Cache.Set(token, authorization, time.Hour*time.Duration(24*14)) + } else { + global.Cache.Set(token, authorization, time.Hour*time.Duration(2)) + } model.UpdateUserById(&model.User{Online: true}, user.ID) diff --git a/pkg/api/asset.go b/pkg/api/asset.go index 68d19c8..2e82d18 100644 --- a/pkg/api/asset.go +++ b/pkg/api/asset.go @@ -112,11 +112,8 @@ func AssetTcpingEndpoint(c echo.Context) (err error) { } active := utils.Tcping(item.IP, item.Port) - asset := model.Asset{ - Active: active, - } - model.UpdateAssetById(&asset, item.ID) + model.UpdateAssetActiveById(active, item.ID) return Success(c, active) } diff --git a/pkg/api/credential.go b/pkg/api/credential.go index 4f18f53..94b22b7 100644 --- a/pkg/api/credential.go +++ b/pkg/api/credential.go @@ -21,6 +21,31 @@ func CredentialCreateEndpoint(c echo.Context) error { item.ID = utils.UUID() item.Created = utils.NowJsonTime() + switch item.Type { + case model.Custom: + item.PrivateKey = "-" + item.Passphrase = "-" + if len(item.Username) == 0 { + item.Username = "-" + } + if len(item.Password) == 0 { + item.Password = "-" + } + case model.PrivateKey: + item.Password = "-" + if len(item.Username) == 0 { + item.Username = "-" + } + if len(item.PrivateKey) == 0 { + item.PrivateKey = "-" + } + if len(item.Passphrase) == 0 { + item.Passphrase = "-" + } + default: + return Fail(c, -1, "类型错误") + } + if err := model.CreateNewCredential(&item); err != nil { return err } @@ -49,6 +74,31 @@ func CredentialUpdateEndpoint(c echo.Context) error { return err } + switch item.Type { + case model.Custom: + item.PrivateKey = "-" + item.Passphrase = "-" + if len(item.Username) == 0 { + item.Username = "-" + } + if len(item.Password) == 0 { + item.Password = "-" + } + case model.PrivateKey: + item.Password = "-" + if len(item.Username) == 0 { + item.Username = "-" + } + if len(item.PrivateKey) == 0 { + item.PrivateKey = "-" + } + if len(item.Passphrase) == 0 { + item.Passphrase = "-" + } + default: + return Fail(c, -1, "类型错误") + } + model.UpdateCredentialById(&item, id) return Success(c, nil) diff --git a/pkg/api/middleware.go b/pkg/api/middleware.go index 25ad9dc..95610b7 100644 --- a/pkg/api/middleware.go +++ b/pkg/api/middleware.go @@ -34,12 +34,19 @@ func Auth(next echo.HandlerFunc) echo.HandlerFunc { } token := GetToken(c) - user, found := global.Cache.Get(token) + authorization, found := global.Cache.Get(token) if !found { logrus.Debugf("您的登录信息已失效,请重新登录后再试。") return Fail(c, 403, "您的登录信息已失效,请重新登录后再试。") } - global.Cache.Set(token, user, time.Minute*time.Duration(30)) + + if authorization.(Authorization).Remember { + // 记住登录有效期两周 + global.Cache.Set(token, authorization, time.Hour*time.Duration(24*14)) + } else { + global.Cache.Set(token, authorization, time.Hour*time.Duration(2)) + } + return next(c) } } diff --git a/pkg/api/routes.go b/pkg/api/routes.go index 7177063..a95fd23 100644 --- a/pkg/api/routes.go +++ b/pkg/api/routes.go @@ -154,7 +154,7 @@ func GetCurrentAccount(c echo.Context) (model.User, bool) { token := GetToken(c) get, b := global.Cache.Get(token) if b { - return get.(model.User), true + return get.(Authorization).User, true } return model.User{}, false } diff --git a/pkg/api/session.go b/pkg/api/session.go index 6b121e5..692dd31 100644 --- a/pkg/api/session.go +++ b/pkg/api/session.go @@ -185,8 +185,14 @@ func SessionCreateEndpoint(c echo.Context) error { return err } - session.Username = credential.Username - session.Password = credential.Password + if credential.Type == model.Custom { + session.Username = credential.Username + session.Password = credential.Password + } else { + session.Username = credential.Username + session.PrivateKey = credential.PrivateKey + session.Passphrase = credential.Passphrase + } } if err := model.CreateNewSession(session); err != nil { @@ -223,12 +229,11 @@ func SessionUploadEndpoint(c echo.Context) error { } dstFile, err := tun.SftpClient.Create(remoteFile) + defer dstFile.Close() if err != nil { return err } - defer dstFile.Close() - buf := make([]byte, 1024) for { n, _ := src.Read(buf) @@ -282,7 +287,9 @@ func SessionDownloadEndpoint(c echo.Context) error { } defer dstFile.Close() - c.Response().Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s", remoteFile)) + // 获取带后缀的文件名称 + filenameWithSuffix := path.Base(remoteFile) + c.Response().Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s", filenameWithSuffix)) var buff bytes.Buffer if _, err := dstFile.WriteTo(&buff); err != nil { diff --git a/pkg/api/ssh.go b/pkg/api/ssh.go index e32cb4b..579a531 100644 --- a/pkg/api/ssh.go +++ b/pkg/api/ssh.go @@ -49,6 +49,7 @@ func (w *NextWriter) Read() ([]byte, int, error) { func SSHEndpoint(c echo.Context) error { ws, err := UpGrader.Upgrade(c.Response().Writer, c.Request(), nil) if err != nil { + logrus.Errorf("升级为WebSocket协议失败:%v", err.Error()) return err } @@ -58,11 +59,13 @@ func SSHEndpoint(c echo.Context) error { sshClient, err := CreateSshClient(assetId) if err != nil { + logrus.Errorf("创建SSH客户端失败:%v", err.Error()) return err } session, err := sshClient.NewSession() if err != nil { + logrus.Errorf("创建SSH会话失败:%v", err.Error()) return err } defer session.Close() @@ -123,36 +126,62 @@ func CreateSshClient(assetId string) (*ssh.Client, error) { return nil, err } + var ( + accountType = asset.AccountType + username = asset.Username + password = asset.Password + privateKey = asset.PrivateKey + passphrase = asset.Passphrase + ) + var authMethod ssh.AuthMethod - if asset.AccountType == "credential" { + if accountType == "credential" { + credential, err := model.FindCredentialById(asset.CredentialId) if err != nil { return nil, err } - asset.Username = credential.Username - asset.Password = credential.Password - authMethod = ssh.Password(asset.Password) - } else if asset.AccountType == "private-key" { + accountType = credential.Type + username = credential.Username + password = credential.Password + privateKey = credential.PrivateKey + passphrase = credential.Passphrase + } + + if username == "-" { + username = "" + } + if password == "-" { + password = "" + } + if privateKey == "-" { + privateKey = "" + } + if passphrase == "-" { + passphrase = "" + } + + if accountType == model.PrivateKey { var key ssh.Signer - if len(asset.Passphrase) > 0 { - key, err = ssh.ParsePrivateKeyWithPassphrase([]byte(asset.PrivateKey), []byte(asset.Passphrase)) + if len(passphrase) > 0 { + key, err = ssh.ParsePrivateKeyWithPassphrase([]byte(privateKey), []byte(passphrase)) if err != nil { return nil, err } } else { - key, err = ssh.ParsePrivateKey([]byte(asset.PrivateKey)) + key, err = ssh.ParsePrivateKey([]byte(privateKey)) if err != nil { return nil, err } } authMethod = ssh.PublicKeys(key) } else { - authMethod = ssh.Password(asset.Password) + authMethod = ssh.Password(password) } config := &ssh.ClientConfig{ Timeout: 1 * time.Second, - User: asset.Username, + User: username, Auth: []ssh.AuthMethod{authMethod}, HostKeyCallback: ssh.InsecureIgnoreHostKey(), } diff --git a/pkg/api/tunnel.go b/pkg/api/tunnel.go index 556eb9e..dfe1d54 100644 --- a/pkg/api/tunnel.go +++ b/pkg/api/tunnel.go @@ -29,6 +29,7 @@ func TunEndpoint(c echo.Context) error { width := c.QueryParam("width") height := c.QueryParam("height") + dpi := c.QueryParam("dpi") sessionId := c.QueryParam("sessionId") connectionId := c.QueryParam("connectionId") @@ -38,6 +39,7 @@ func TunEndpoint(c echo.Context) error { configuration := guacd.NewConfiguration() configuration.SetParameter("width", width) configuration.SetParameter("height", height) + configuration.SetParameter("dpi", dpi) propertyMap := model.FindAllPropertiesMap() @@ -73,8 +75,6 @@ func TunEndpoint(c echo.Context) error { configuration.SetParameter("security", "any") configuration.SetParameter("ignore-cert", "true") configuration.SetParameter("create-drive-path", "true") - - configuration.SetParameter("dpi", "96") configuration.SetParameter("resize-method", "reconnect") configuration.SetParameter(guacd.EnableDrive, propertyMap[guacd.EnableDrive]) configuration.SetParameter(guacd.DriveName, propertyMap[guacd.DriveName]) @@ -99,9 +99,7 @@ func TunEndpoint(c echo.Context) error { configuration.SetParameter("password", session.Password) } - fontSize, _ := strconv.Atoi(propertyMap[guacd.FontSize]) - fontSize = fontSize * 2 - configuration.SetParameter(guacd.FontSize, strconv.Itoa(fontSize)) + configuration.SetParameter(guacd.FontSize, propertyMap[guacd.FontSize]) configuration.SetParameter(guacd.FontName, propertyMap[guacd.FontName]) configuration.SetParameter(guacd.ColorScheme, propertyMap[guacd.ColorScheme]) break diff --git a/pkg/guacd/guacd.go b/pkg/guacd/guacd.go index ca9d172..fc4d680 100644 --- a/pkg/guacd/guacd.go +++ b/pkg/guacd/guacd.go @@ -134,8 +134,10 @@ func NewTunnel(address string, config Configuration) (ret *Tunnel, err error) { width := config.GetParameter("width") height := config.GetParameter("height") + dpi := config.GetParameter("dpi") + // send size - if err := ret.WriteInstructionAndFlush(NewInstruction("size", width, height, "96")); err != nil { + if err := ret.WriteInstructionAndFlush(NewInstruction("size", width, height, dpi)); err != nil { return nil, err } diff --git a/pkg/handle/runner.go b/pkg/handle/runner.go index 68d92dc..d5f68d1 100644 --- a/pkg/handle/runner.go +++ b/pkg/handle/runner.go @@ -10,7 +10,7 @@ import ( func RunTicker() { var ch chan int - //定时任务 + // 定时任务,每隔一小时删除一次未使用的会话信息 ticker := time.NewTicker(time.Minute * 60) go func() { for range ticker.C { diff --git a/pkg/model/asset.go b/pkg/model/asset.go index a138a1a..aa884f1 100644 --- a/pkg/model/asset.go +++ b/pkg/model/asset.go @@ -85,6 +85,11 @@ func UpdateAssetById(o *Asset, id string) { global.DB.Updates(o) } +func UpdateAssetActiveById(active bool, id string) { + sql := "update assets set active = ? where id = ?" + global.DB.Exec(sql, active, id) +} + func DeleteAssetById(id string) { global.DB.Where("id = ?", id).Delete(&Asset{}) } diff --git a/pkg/model/credential.go b/pkg/model/credential.go index d2409f2..c8d1f6b 100644 --- a/pkg/model/credential.go +++ b/pkg/model/credential.go @@ -5,12 +5,21 @@ import ( "next-terminal/pkg/utils" ) +// 密码 +const Custom = "custom" + +// 密钥 +const PrivateKey = "private-key" + type Credential struct { - ID string `gorm:"primary_key" json:"id"` - Name string `json:"name"` - Username string `json:"username"` - Password string `json:"password"` - Created utils.JsonTime `json:"created"` + ID string `gorm:"primary_key" json:"id"` + Name string `json:"name"` + Type string `json:"type"` + Username string `json:"username"` + Password string `json:"password"` + PrivateKey string `json:"privateKey"` + Passphrase string `json:"passphrase"` + Created utils.JsonTime `json:"created"` } func (r *Credential) TableName() string { diff --git a/web/package.json b/web/package.json index ddd757d..e89a1be 100644 --- a/web/package.json +++ b/web/package.json @@ -1,6 +1,6 @@ { "name": "next-terminal", - "version": "0.0.4", + "version": "0.0.5", "private": true, "dependencies": { "@ant-design/icons": "^4.3.0", diff --git a/web/src/common/constants.js b/web/src/common/constants.js index 6b81e3c..4ea3897 100644 --- a/web/src/common/constants.js +++ b/web/src/common/constants.js @@ -1,6 +1,6 @@ // prod -export const server = ''; -export const wsServer = ''; +export const server = '/'; +export const wsServer = '/'; export const prefix = ''; // dev diff --git a/web/src/components/Login.js b/web/src/components/Login.js index 5385d52..e54a185 100644 --- a/web/src/components/Login.js +++ b/web/src/components/Login.js @@ -41,13 +41,6 @@ class LoginForm extends Component { localStorage.setItem('X-Auth-Token', result['data']); // this.props.history.push(); window.location.href = "/" - - let r = await request.get('/info'); - if (r.code === 1) { - this.props.updateUser(r.data); - } else { - message.error(r.message); - } } catch (e) { message.error(e.message); } finally { diff --git a/web/src/components/access/Access.js b/web/src/components/access/Access.js index 82e1c67..f348092 100644 --- a/web/src/components/access/Access.js +++ b/web/src/components/access/Access.js @@ -308,7 +308,6 @@ class Access extends Component { if (navigator.clipboard) { await navigator.clipboard.writeText(data); } - }; } @@ -433,6 +432,10 @@ class Access extends Component { let width = window.innerWidth; let height = window.innerHeight; + let dpi = 96; + if (protocol === 'ssh' || protocol === 'telnet') { + dpi = dpi * 2; + } let token = getToken(); @@ -440,6 +443,7 @@ class Access extends Component { 'sessionId': sessionId, 'width': width, 'height': height, + 'dpi': dpi, 'X-Auth-Token': token }; @@ -848,6 +852,7 @@ class Access extends Component {