diff --git a/README.md b/README.md
index ecd9036..4b72f1f 100644
--- a/README.md
+++ b/README.md
@@ -45,7 +45,10 @@ test/test
如果您觉得 next-terminal 对你有帮助,欢迎给予我们一定的捐助来维持项目的长期发展。
-
+
+
+
+
## 联系方式
@@ -53,8 +56,6 @@ test/test
- QQ群 938145268
-
-
- Telegram
https://t.me/next_terminal
\ No newline at end of file
diff --git a/main.go b/main.go
index 0ad83c8..a359535 100644
--- a/main.go
+++ b/main.go
@@ -19,6 +19,7 @@ import (
"next-terminal/pkg/utils"
"os"
"strconv"
+ "strings"
"time"
)
@@ -166,8 +167,11 @@ func Run() error {
// 配置缓存器
global.Cache = cache.New(5*time.Minute, 10*time.Minute)
global.Cache.OnEvicted(func(key string, value interface{}) {
- logrus.Debugf("用户Token「%v」过期", key)
- model.Logout(key)
+ if strings.HasPrefix(key, api.Token) {
+ token := strings.Split(key, ":")[0]
+ logrus.Debugf("用户Token「%v」过期", token)
+ model.Logout(token)
+ }
})
global.Store = global.NewStore()
@@ -191,11 +195,13 @@ func Run() error {
User: user,
}
+ cacheKey := strings.Join([]string{api.Token, token}, ":")
+
if authorization.Remember {
// 记住登录有效期两周
- global.Cache.Set(token, authorization, api.RememberEffectiveTime)
+ global.Cache.Set(cacheKey, authorization, api.RememberEffectiveTime)
} else {
- global.Cache.Set(token, authorization, api.NotRememberEffectiveTime)
+ global.Cache.Set(cacheKey, authorization, api.NotRememberEffectiveTime)
}
logrus.Debugf("重新加载用户「%v」授权Token「%v」到缓存", user.Nickname, token)
}
diff --git a/pkg/api/account.go b/pkg/api/account.go
index a045f08..4b3ed06 100644
--- a/pkg/api/account.go
+++ b/pkg/api/account.go
@@ -46,19 +46,35 @@ func LoginEndpoint(c echo.Context) error {
}
user, err := model.FindUserByUsername(loginAccount.Username)
+
+ // 存储登录失败次数信息
+ loginFailCountKey := loginAccount.Username
+ v, ok := global.Cache.Get(loginFailCountKey)
+ if !ok {
+ v = 1
+ }
+ count := v.(int)
+ if count >= 5 {
+ return Fail(c, -1, "登录失败次数过多,请稍后再试")
+ }
+
if err != nil {
- return Fail(c, -1, "您输入的账号或密码不正确")
+ count++
+ global.Cache.Set(loginFailCountKey, count, time.Minute*time.Duration(5))
+ return FailWithData(c, -1, "您输入的账号或密码不正确", count)
}
if err := utils.Encoder.Match([]byte(user.Password), []byte(loginAccount.Password)); err != nil {
- return Fail(c, -1, "您输入的账号或密码不正确")
+ count++
+ global.Cache.Set(loginFailCountKey, count, time.Minute*time.Duration(5))
+ return FailWithData(c, -1, "您输入的账号或密码不正确", count)
}
if user.TOTPSecret != "" && user.TOTPSecret != "-" {
return Fail(c, 0, "")
}
- token, err := Login(c, loginAccount, user)
+ token, err := LoginSuccess(c, loginAccount, user)
if err != nil {
return err
}
@@ -66,7 +82,7 @@ func LoginEndpoint(c echo.Context) error {
return Success(c, token)
}
-func Login(c echo.Context, loginAccount LoginAccount, user model.User) (token string, err error) {
+func LoginSuccess(c echo.Context, loginAccount LoginAccount, user model.User) (token string, err error) {
token = strings.Join([]string{utils.UUID(), utils.UUID(), utils.UUID(), utils.UUID()}, "")
authorization := Authorization{
@@ -75,11 +91,13 @@ func Login(c echo.Context, loginAccount LoginAccount, user model.User) (token st
User: user,
}
+ cacheKey := strings.Join([]string{Token, token}, ":")
+
if authorization.Remember {
// 记住登录有效期两周
- global.Cache.Set(token, authorization, RememberEffectiveTime)
+ global.Cache.Set(cacheKey, authorization, RememberEffectiveTime)
} else {
- global.Cache.Set(token, authorization, NotRememberEffectiveTime)
+ global.Cache.Set(cacheKey, authorization, NotRememberEffectiveTime)
}
// 保存登录日志
@@ -107,20 +125,37 @@ func loginWithTotpEndpoint(c echo.Context) error {
return err
}
+ // 存储登录失败次数信息
+ loginFailCountKey := loginAccount.Username
+ v, ok := global.Cache.Get(loginFailCountKey)
+ if !ok {
+ v = 1
+ }
+ count := v.(int)
+ if count >= 5 {
+ return Fail(c, -1, "登录失败次数过多,请稍后再试")
+ }
+
user, err := model.FindUserByUsername(loginAccount.Username)
if err != nil {
- return Fail(c, -1, "您输入的账号或密码不正确")
+ count++
+ global.Cache.Set(loginFailCountKey, count, time.Minute*time.Duration(5))
+ return FailWithData(c, -1, "您输入的账号或密码不正确", count)
}
if err := utils.Encoder.Match([]byte(user.Password), []byte(loginAccount.Password)); err != nil {
- return Fail(c, -1, "您输入的账号或密码不正确")
+ count++
+ global.Cache.Set(loginFailCountKey, count, time.Minute*time.Duration(5))
+ return FailWithData(c, -1, "您输入的账号或密码不正确", count)
}
if !totp.Validate(loginAccount.TOTP, user.TOTPSecret) {
- return Fail(c, -2, "您的TOTP不匹配")
+ count++
+ global.Cache.Set(loginFailCountKey, count, time.Minute*time.Duration(5))
+ return FailWithData(c, -1, "您输入双因素认证授权码不正确", count)
}
- token, err := Login(c, loginAccount, user)
+ token, err := LoginSuccess(c, loginAccount, user)
if err != nil {
return err
}
diff --git a/pkg/api/middleware.go b/pkg/api/middleware.go
index f0ea3c0..8e925e5 100644
--- a/pkg/api/middleware.go
+++ b/pkg/api/middleware.go
@@ -41,7 +41,8 @@ func Auth(next echo.HandlerFunc) echo.HandlerFunc {
}
token := GetToken(c)
- authorization, found := global.Cache.Get(token)
+ cacheKey := strings.Join([]string{Token, token}, ":")
+ authorization, found := global.Cache.Get(cacheKey)
if !found {
return Fail(c, 401, "您的登录信息已失效,请重新登录后再试。")
}
diff --git a/pkg/api/routes.go b/pkg/api/routes.go
index 6be1300..3caf5d6 100644
--- a/pkg/api/routes.go
+++ b/pkg/api/routes.go
@@ -156,6 +156,14 @@ func Fail(c echo.Context, code int, message string) error {
})
}
+func FailWithData(c echo.Context, code int, message string, data interface{}) error {
+ return c.JSON(200, H{
+ "code": code,
+ "message": message,
+ "data": data,
+ })
+}
+
func Success(c echo.Context, data interface{}) error {
return c.JSON(200, H{
"code": 1,
diff --git a/pkg/api/session.go b/pkg/api/session.go
index 3cbffa1..fce379d 100644
--- a/pkg/api/session.go
+++ b/pkg/api/session.go
@@ -66,15 +66,9 @@ func SessionPagingEndpoint(c echo.Context) error {
func SessionDeleteEndpoint(c echo.Context) error {
sessionIds := c.Param("id")
split := strings.Split(sessionIds, ",")
- for i := range split {
- drivePath, err := model.GetRecordingPath()
- if err != nil {
- continue
- }
- if err := os.RemoveAll(path.Join(drivePath, split[i])); err != nil {
- return err
- }
- model.DeleteSessionById(split[i])
+ err := model.DeleteSessionByIds(split)
+ if err != nil {
+ return err
}
return Success(c, nil)
@@ -132,7 +126,7 @@ func CloseSessionById(sessionId string, code int, reason string) {
if s.Status == model.Connecting {
// 会话还未建立成功,无需保留数据
- model.DeleteSessionById(sessionId)
+ _ = model.DeleteSessionById(sessionId)
return
}
diff --git a/pkg/guacd/guacd.go b/pkg/guacd/guacd.go
index c767f90..0db2947 100644
--- a/pkg/guacd/guacd.go
+++ b/pkg/guacd/guacd.go
@@ -254,6 +254,9 @@ func (opt *Tunnel) Read() (p []byte, err error) {
if s == "rate=44100,channels=2;" {
return make([]byte, 0), nil
}
+ if s == "rate=22050,channels=2;" {
+ return make([]byte, 0), nil
+ }
if s == "5.audio,1.1,31.audio/L16;" {
s += "rate=44100,channels=2;"
}
diff --git a/pkg/handle/runner.go b/pkg/handle/runner.go
index 109635c..b355e04 100644
--- a/pkg/handle/runner.go
+++ b/pkg/handle/runner.go
@@ -3,6 +3,7 @@ package handle
import (
"github.com/robfig/cron/v3"
"github.com/sirupsen/logrus"
+ "log"
"next-terminal/pkg/guacd"
"next-terminal/pkg/model"
"next-terminal/pkg/utils"
@@ -22,7 +23,7 @@ func RunTicker() {
now := time.Now()
for i := range sessions {
if now.Sub(sessions[i].ConnectedTime.Time) > time.Hour*1 {
- model.DeleteSessionById(sessions[i].ID)
+ _ = model.DeleteSessionById(sessions[i].ID)
s := sessions[i].Username + "@" + sessions[i].IP + ":" + strconv.Itoa(sessions[i].Port)
logrus.Infof("会话「%v」ID「%v」超过1小时未打开,已删除。", s, sessions[i].ID)
}
@@ -40,6 +41,40 @@ func RunTicker() {
}
})
+ _, err := c.AddFunc("0 0 0 * * ?", func() {
+ // 定时任务 每日凌晨检查超过时长限制的会话
+ property, err := model.FindPropertyByName("session-saved-limit")
+ if err != nil {
+ return
+ }
+ if property.Value == "" || property.Value == "-" {
+ return
+ }
+ limit, err := strconv.Atoi(property.Value)
+ if err != nil {
+ return
+ }
+ sessions, err := model.FindOutTimeSessions(limit)
+ if err != nil {
+ return
+ }
+
+ if sessions != nil && len(sessions) > 0 {
+ var sessionIds []string
+ for i := range sessions {
+ sessionIds = append(sessionIds, sessions[i].ID)
+ }
+ err := model.DeleteSessionByIds(sessionIds)
+ if err != nil {
+ logrus.Errorf("删除离线会话失败 %v", err)
+ }
+ }
+ })
+
+ if err != nil {
+ log.Fatal(err)
+ }
+
c.Start()
}
diff --git a/pkg/model/session.go b/pkg/model/session.go
index c2fec30..b0cbf08 100644
--- a/pkg/model/session.go
+++ b/pkg/model/session.go
@@ -3,6 +3,8 @@ package model
import (
"next-terminal/pkg/global"
"next-terminal/pkg/utils"
+ "os"
+ "path"
"time"
)
@@ -126,6 +128,12 @@ func FindSessionByStatusIn(statuses []string) (o []Session, err error) {
return
}
+func FindOutTimeSessions(dayLimit int) (o []Session, err error) {
+ limitTime := time.Now().Add(time.Duration(-dayLimit*24) * time.Hour)
+ err = global.DB.Where("status = ? and connected_time < ?", Disconnected, limitTime).Find(&o).Error
+ return
+}
+
func CreateNewSession(o *Session) (err error) {
err = global.DB.Create(o).Error
return
@@ -154,8 +162,24 @@ func UpdateSessionWindowSizeById(width, height int, id string) error {
return UpdateSessionById(&session, id)
}
-func DeleteSessionById(id string) {
- global.DB.Where("id = ?", id).Delete(&Session{})
+func DeleteSessionById(id string) error {
+ return global.DB.Where("id = ?", id).Delete(&Session{}).Error
+}
+
+func DeleteSessionByIds(sessionIds []string) error {
+ drivePath, err := GetRecordingPath()
+ if err != nil {
+ return err
+ }
+ for i := range sessionIds {
+ if err := os.RemoveAll(path.Join(drivePath, sessionIds[i])); err != nil {
+ return err
+ }
+ if err := DeleteSessionById(sessionIds[i]); err != nil {
+ return err
+ }
+ }
+ return nil
}
func DeleteSessionByStatus(status string) {
diff --git a/screenshot/donate.png b/screenshot/donate.png
deleted file mode 100644
index 381a9e0..0000000
Binary files a/screenshot/donate.png and /dev/null differ
diff --git a/screenshot/donate_wx.png b/screenshot/donate_wx.png
new file mode 100644
index 0000000..be86d0f
Binary files /dev/null and b/screenshot/donate_wx.png differ
diff --git a/screenshot/donate_zfb.png b/screenshot/donate_zfb.png
new file mode 100644
index 0000000..2fe9da0
Binary files /dev/null and b/screenshot/donate_zfb.png differ
diff --git a/web/src/components/access/Access.js b/web/src/components/access/Access.js
index d748635..4a40dc3 100644
--- a/web/src/components/access/Access.js
+++ b/web/src/components/access/Access.js
@@ -314,9 +314,6 @@ class Access extends Component {
};
onKeyDown = (keysym) => {
- // if (this.state.clipboardVisible || this.state.fileSystemVisible) {
- // return true;
- // }
this.state.client.sendKeyEvent(1, keysym);
if (keysym === 65288) {
return false;
@@ -426,6 +423,7 @@ class Access extends Component {
const sink = new Guacamole.InputSink();
display.appendChild(sink.getElement());
+ sink.focus();
// Keyboard
const keyboard = new Guacamole.Keyboard(sink.getElement());
@@ -582,7 +580,7 @@ class Access extends Component {
-
@@ -590,7 +588,7 @@ class Access extends Component {
- } onClick={() => {
+ } disabled={this.state.clientState !== STATE_CONNECTED} onClick={() => {
this.setState({
clipboardVisible: true
});
@@ -603,7 +601,7 @@ class Access extends Component {
<>
- } onClick={() => {
+ } disabled={this.state.clientState !== STATE_CONNECTED} onClick={() => {
this.setState({
fileSystemVisible: true,
});
@@ -613,8 +611,8 @@ class Access extends Component {
-
- }/>
+
+ } disabled={this.state.clientState !== STATE_CONNECTED}/>
@@ -626,7 +624,7 @@ class Access extends Component {
<>
- } onClick={() => {
+ } disabled={this.state.clientState !== STATE_CONNECTED} onClick={() => {
this.setState({
fileSystemVisible: true,
});
diff --git a/web/src/components/access/FileSystem.js b/web/src/components/access/FileSystem.js
index f3104be..fad8926 100644
--- a/web/src/components/access/FileSystem.js
+++ b/web/src/components/access/FileSystem.js
@@ -37,6 +37,7 @@ class FileSystem extends Component {
state = {
sessionId: undefined,
currentDirectory: '/',
+ currentDirectoryInput: '/',
files: [],
loading: false,
selectedRowKeys: [],
@@ -126,6 +127,7 @@ class FileSystem extends Component {
this.setState({
files: items,
currentDirectory: key,
+ currentDirectoryInput: key,
selectedRow: {},
selectedRowKeys: []
})
@@ -189,6 +191,16 @@ class FileSystem extends Component {
return undefined;
};
+ handleCurrentDirectoryInputChange = (event) => {
+ this.setState({
+ currentDirectoryInput: event.target.value
+ })
+ }
+
+ handleCurrentDirectoryInputPressEnter = (event) => {
+ this.loadFiles(event.target.value);
+ }
+
render() {
const columns = [
@@ -315,10 +327,11 @@ class FileSystem extends Component {
const title = (
-
- {this.state.currentDirectory}
+
+
-
+
}
@@ -383,7 +396,6 @@ class FileSystem extends Component {
if (record['path'] === '..') {
// 获取当前目录的上级目录
let currentDirectory = this.state.currentDirectory;
- console.log(currentDirectory)
let parentDirectory = currentDirectory.substring(0, currentDirectory.lastIndexOf('/'));
this.loadFiles(parentDirectory);
} else {
diff --git a/web/src/components/setting/Setting.js b/web/src/components/setting/Setting.js
index 452199c..b7d5186 100644
--- a/web/src/components/setting/Setting.js
+++ b/web/src/components/setting/Setting.js
@@ -546,6 +546,21 @@ class Setting extends Component {
> : null
}
+
+
+
+
更新