diff --git a/entrypoint.sh b/entrypoint.sh index d11875706..10434b5cc 100644 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -6,6 +6,16 @@ do echo "wait for jms_core $CORE_HOST ready" sleep 2 done +# 限制所有可执行目录的权限 +chmod -R 700 /usr/local/sbin/* && chmod -R 700 /usr/local/bin/* && chmod -R 700 /usr/bin/* +chmod -R 700 /usr/sbin/* && chmod -R 700 /sbin/* && chmod -R 700 /bin/* + +# 放开部分需要的可执行权限 +chmod 755 `which mysql` `which psql` `which mongosh` `which tsql` `which redis` `which clickhouse-client` +chmod 755 `which kubectl` `which rawkubectl` `which helm` `which rawhelm` +chmod 755 `which jq` `which less` `which vim` `which ls` `which bash` +# k8s 集群连接需要的命令 +chmod 755 `which grep` cd /opt/koko ./koko diff --git a/go.mod b/go.mod index 9b028076a..a6ef023e2 100644 --- a/go.mod +++ b/go.mod @@ -33,9 +33,9 @@ require ( github.com/spf13/viper v1.12.0 github.com/xlab/treeprint v1.1.0 go.mongodb.org/mongo-driver v1.8.3 - golang.org/x/crypto v0.9.0 - golang.org/x/term v0.8.0 - golang.org/x/text v0.9.0 + golang.org/x/crypto v0.14.0 + golang.org/x/term v0.13.0 + golang.org/x/text v0.13.0 gopkg.in/natefinch/lumberjack.v2 v2.0.0 k8s.io/api v0.23.1 k8s.io/apimachinery v0.23.1 @@ -103,10 +103,10 @@ require ( github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect github.com/yusufpapurcu/wmi v1.2.2 // indirect golang.org/x/arch v0.3.0 // indirect - golang.org/x/net v0.10.0 // indirect + golang.org/x/net v0.17.0 // indirect golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5 // indirect golang.org/x/sync v0.1.0 // indirect - golang.org/x/sys v0.8.0 // indirect + golang.org/x/sys v0.13.0 // indirect golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df // indirect google.golang.org/appengine v1.6.7 // indirect @@ -125,6 +125,6 @@ require ( ) replace ( - github.com/gliderlabs/ssh => github.com/LeeEirc/ssh v0.1.2-0.20220323091501-23b956e1e5a8 + github.com/gliderlabs/ssh => github.com/LeeEirc/ssh v0.1.2-0.20231007053448-a6110c0dfc4a golang.org/x/crypto => github.com/LeeEirc/crypto v0.0.0-20230919154755-059031d26b68 ) diff --git a/go.sum b/go.sum index f329aacdd..585365b7b 100644 --- a/go.sum +++ b/go.sum @@ -65,8 +65,8 @@ github.com/LeeEirc/elfinder v0.0.14 h1:6ObxwIoC5zmrnKArUU5Mz++/T3lzgl1Ja0pS1Smd3 github.com/LeeEirc/elfinder v0.0.14/go.mod h1:d1bMAAydkZSBxSN/EuQjBg6B0xcPP3boHuYEpzEHYTs= github.com/LeeEirc/httpsig v1.2.1 h1:GGmCc2Bug3KeCchlZHwrfyjyAnw+JlzMjKDobPypirs= github.com/LeeEirc/httpsig v1.2.1/go.mod h1:aoLZLXCSNDgkzsH2sGLWn3hlVbF+Voe8fCArxLt9nWA= -github.com/LeeEirc/ssh v0.1.2-0.20220323091501-23b956e1e5a8 h1:UxED5pKJd9yel/LXEUHDn8C+pYhDogxwx7G9HZcov4w= -github.com/LeeEirc/ssh v0.1.2-0.20220323091501-23b956e1e5a8/go.mod h1:bSl4MzlGJ2FbMCzfyuwruG2mrWY0dxE8wqWoAIhKe8k= +github.com/LeeEirc/ssh v0.1.2-0.20231007053448-a6110c0dfc4a h1:/EdJeCK6cTaKNgftQLP9uyBL4Q86MFawU0WsK22yn2A= +github.com/LeeEirc/ssh v0.1.2-0.20231007053448-a6110c0dfc4a/go.mod h1:O9BMs9PYwCJbftRP9O2Ig5Wd3hbLSpzhvP0bqU9EONg= github.com/LeeEirc/tclientlib v0.0.3-0.20230803101925-fb52a90cb08d h1:4qUSGc/34IALiDs2kBrjbCKfx7zvAt16K+gTRzNN8Fo= github.com/LeeEirc/tclientlib v0.0.3-0.20230803101925-fb52a90cb08d/go.mod h1:TF2v0XZYyRcZfx4NmA/EEFRkdKZLsQd8YnlhGKl1KUA= github.com/LeeEirc/terminalparser v0.0.0-20220328021224-de16b7643ea4 h1:Gk7m4Nu2jqVqJAJqNlTYqkiq96WkANAtB4fVi+t7Xv8= @@ -523,8 +523,8 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= -golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= -golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= +golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -612,16 +612,16 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= -golang.org/x/term v0.8.0 h1:n5xxQn2i3PC0yLAbjTpNT85q/Kgzcr2gIoX9OrJUols= -golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek= +golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -634,8 +634,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= -golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= diff --git a/pkg/auth/ssh.go b/pkg/auth/ssh.go index ff9dd30a7..be31acee6 100644 --- a/pkg/auth/ssh.go +++ b/pkg/auth/ssh.go @@ -1,10 +1,12 @@ package auth import ( + "errors" "net" "strings" "github.com/gliderlabs/ssh" + "github.com/jumpserver/koko/pkg/config" gossh "golang.org/x/crypto/ssh" "github.com/jumpserver/koko/pkg/jms-sdk-go/model" @@ -36,12 +38,15 @@ func SSHPasswordAndPublicKeyAuth(jmsService *service.JMService) SSHAuthFunc { userAuthClient, ok := ctx.Value(ContextKeyClient).(*UserAuthClient) if !ok { newClient := jmsService.CloneClient() - + var accessKey model.AccessKey + conf := config.GetConf() + _ = accessKey.LoadFromFile(conf.AccessKeyFilePath) userClient := service.NewUserClient( service.UserClientUsername(username), service.UserClientRemoteAddr(remoteAddr), service.UserClientLoginType("T"), service.UserClientHttpClient(&newClient), + service.UserClientSvcSignKey(accessKey), ) userAuthClient = &UserAuthClient{ UserClient: userClient, @@ -78,6 +83,13 @@ func SSHKeyboardInteractiveAuth(ctx ssh.Context, challenger gossh.KeyboardIntera if value, ok := ctx.Value(ContextKeyAuthFailed).(*bool); ok && *value { return ssh.AuthFailed } + // 2 steps auth must have a partial success method + if val := ctx.Value(ContextKeyPartialSuccessMethod); val == nil { + logger.Errorf("SSH conn[%s] user %s Mfa Auth failed: not found partial success method.", + ctx.SessionID(), ctx.User()) + return ssh.AuthFailed + } + username := GetUsernameFromSSHCtx(ctx) res = ssh.AuthFailed client, ok := ctx.Value(ContextKeyClient).(*UserAuthClient) @@ -104,6 +116,19 @@ func SSHKeyboardInteractiveAuth(ctx ssh.Context, challenger gossh.KeyboardIntera return } +func SSHAuthLogCallback(ctx ssh.Context, method string, err error) { + if err == nil { + logger.Errorf("SSH conn[%s] auth method %s success", ctx.SessionID(), method) + return + } + if errors.Is(err, gossh.ErrPartialSuccess) { + ctx.SetValue(ContextKeyPartialSuccessMethod, method) + logger.Infof("SSH conn[%s] auth method %s partially success", ctx.SessionID(), method) + } else { + logger.Errorf("SSH conn[%s] auth method %s failed: %s", ctx.SessionID(), method, err) + } +} + const ( ContextKeyUser = "CONTEXT_USER" ContextKeyClient = "CONTEXT_CLIENT" @@ -113,6 +138,8 @@ const ( ContextKeyAuthFailed = "CONTEXT_AUTH_FAILED" ContextKeyDirectLoginFormat = "CONTEXT_DIRECT_LOGIN_FORMAT" + + ContextKeyPartialSuccessMethod = "CONTEXT_PARTIAL_SUCCESS_METHOD" ) type DirectLoginAssetReq struct { diff --git a/pkg/exchange/redis.go b/pkg/exchange/redis.go index 3da857a6e..8cc4a3bc7 100644 --- a/pkg/exchange/redis.go +++ b/pkg/exchange/redis.go @@ -136,6 +136,8 @@ func newRedisManager(cfg Config) (*redisRoomManager, error) { sentinelOpts = append(sentinelOpts, dialOptions...) if cfg.SentinelPassword != "" { sentinelOpts = append(sentinelOpts, radix.DialAuthPass(cfg.SentinelPassword)) + } else { + sentinelOpts = append(sentinelOpts, radix.DialAuthUser("", "")) } sentinelConnFunc := func(network, addr string) (radix.Conn, error) { conn, err := radix.Dial(network, addr, sentinelOpts...) diff --git a/pkg/handler/asset.go b/pkg/handler/asset.go index 898dbb47c..dd0c9ed74 100644 --- a/pkg/handler/asset.go +++ b/pkg/handler/asset.go @@ -175,6 +175,7 @@ func (u *UserSelectHandler) proxyAsset(asset model.Asset) { Account: selectedAccount.Alias, Protocol: protocol, ConnectMethod: "ssh", + RemoteAddr: u.h.sess.RemoteAddr(), } tokenInfo, err := u.h.jmsService.CreateSuperConnectToken(&req) if err != nil { diff --git a/pkg/handler/direct_handler.go b/pkg/handler/direct_handler.go index 666e58f32..42ce3351d 100644 --- a/pkg/handler/direct_handler.go +++ b/pkg/handler/direct_handler.go @@ -390,6 +390,7 @@ func (d *DirectHandler) Proxy(asset model.Asset) { Account: selectAccount.Alias, Protocol: protocol, ConnectMethod: model.ProtocolSSH, + RemoteAddr: d.wrapperSess.RemoteAddr(), } tokenInfo, err := d.jmsService.CreateSuperConnectToken(&req) if err != nil { diff --git a/pkg/handler/server_ssh.go b/pkg/handler/server_ssh.go index b68625cd4..4f4913a9f 100644 --- a/pkg/handler/server_ssh.go +++ b/pkg/handler/server_ssh.go @@ -239,12 +239,14 @@ func (s *Server) SessionHandler(sess ssh.Session) { func (s *Server) proxyDirectRequest(sess ssh.Session, user *model.User, asset model.Asset, permAccount model.PermAccount) { // 仅支持 ssh 的协议资产 + remoteAddr, _, _ := net.SplitHostPort(sess.RemoteAddr().String()) req := &service.SuperConnectTokenReq{ UserId: user.ID, AssetId: asset.ID, Account: permAccount.Alias, Protocol: model.ProtocolSSH, ConnectMethod: model.ProtocolSSH, + RemoteAddr: remoteAddr, } // ssh 非交互式的直连格式,不支持资产的登录复核 tokenInfo, err := s.jmsService.CreateSuperConnectToken(req) diff --git a/pkg/httpd/message.go b/pkg/httpd/message.go index e328e90fe..eb720892d 100644 --- a/pkg/httpd/message.go +++ b/pkg/httpd/message.go @@ -1,9 +1,10 @@ package httpd import ( - "github.com/jumpserver/koko/pkg/exchange" "time" + "github.com/jumpserver/koko/pkg/exchange" + "github.com/jumpserver/koko/pkg/jms-sdk-go/model" ) @@ -40,7 +41,11 @@ const ( TerminalShareUserRemove = "TERMINAL_SHARE_USER_REMOVE" + TerminalSyncUserPreference = "TERMINAL_SYNC_USER_PREFERENCE" + TerminalError = "TERMINAL_ERROR" + + MessageNotify = "MESSAGE_NOTIFY" ) type WindowSize struct { @@ -80,6 +85,10 @@ type ShareInfo struct { Record model.ShareRecord } +type UserKoKoPreferenceParam struct { + ThemeName string `json:"terminal_theme_name"` +} + const ( TargetTypeMonitor = "monitor" diff --git a/pkg/httpd/tty.go b/pkg/httpd/tty.go index 4faac917b..7b9ff088f 100644 --- a/pkg/httpd/tty.go +++ b/pkg/httpd/tty.go @@ -7,7 +7,6 @@ import ( "sync" "github.com/gliderlabs/ssh" - "github.com/jumpserver/koko/pkg/exchange" "github.com/jumpserver/koko/pkg/jms-sdk-go/common" "github.com/jumpserver/koko/pkg/jms-sdk-go/model" @@ -186,6 +185,17 @@ func (h *tty) handleTerminalMessage(msg *Message) { logger.Debugf("Ws[%s] receive share remove user request %s", h.ws.Uuid, msg.Data) go h.removeShareUser(&query) return + case TerminalSyncUserPreference: + var preference UserKoKoPreferenceParam + err := json.Unmarshal([]byte(msg.Data), &preference) + if err != nil { + logger.Errorf("Ws[%s] message(%s) data unmarshal err: %s", h.ws.Uuid, + msg.Type, msg.Data) + return + } + logger.Debugf("Ws[%s] receive sync user preference request %s", h.ws.Uuid, msg.Data) + go h.syncUserPreference(&preference) + return case CLOSE: _ = h.backendClient.Close() default: @@ -209,6 +219,41 @@ func (h *tty) removeShareUser(query *RemoveSharingUserParams) { } } +func (h *tty) syncUserPreference(preference *UserKoKoPreferenceParam) { + /* + {"basic":{"file_name_conflict_resolution":"replace","terminal_theme_name":"Flat"}} + */ + reqCookies := h.ws.ctx.Request.Cookies() + var cookies = make(map[string]string) + for _, cookie := range reqCookies { + cookies[cookie.Name] = cookie.Value + } + data := model.UserKokoPreference{ + Basic: model.KokoBasic{ + ThemeName: preference.ThemeName, + }, + } + var msg struct { + EventName string `json:"event_name"` + } + msg.EventName = "sync_user_preference" + errMsg := "" + err := h.ws.apiClient.SyncUserKokoPreference(cookies, data) + if err != nil { + logger.Errorf("Ws[%s] sync user preference err: %s", h.ws.Uuid, err) + errMsg = err.Error() + } + msgNotify, _ := json.Marshal(msg) + + h.ws.SendMessage(&Message{ + Id: h.ws.Uuid, + Type: MessageNotify, + Data: string(msgNotify), + Err: errMsg, + }) + +} + func (h *tty) createShareSession(shareData *ShareRequestParams) { // 创建 共享连接 res, err := h.handleShareRequest(shareData) diff --git a/pkg/jms-sdk-go/model/session.go b/pkg/jms-sdk-go/model/session.go index ec2f7ed30..65b01bc2b 100644 --- a/pkg/jms-sdk-go/model/session.go +++ b/pkg/jms-sdk-go/model/session.go @@ -86,3 +86,15 @@ func ParseReplayVersion(gzFile string, defaultValue ReplayVersion) ReplayVersion } return defaultValue } + +type ReplayError LabelField + +func (r ReplayError) Error() string { + return string(r) +} + +const ( + SessionReplayErrConnectFailed ReplayError = "connect_failed" + SessionReplayErrCreatedFailed ReplayError = "replay_create_failed" + SessionReplayErrUploadFailed ReplayError = "replay_upload_failed" +) diff --git a/pkg/jms-sdk-go/model/token.go b/pkg/jms-sdk-go/model/token.go index 9e349516f..e1dfc1f2b 100644 --- a/pkg/jms-sdk-go/model/token.go +++ b/pkg/jms-sdk-go/model/token.go @@ -72,4 +72,5 @@ type ConnectOptions struct { BackspaceAsCtrlH *bool `json:"backspaceAsCtrlH,omitempty"` FilenameConflictResolution string `json:"file_name_conflict_resolution,omitempty"` + TerminalThemeName string `json:"terminal_theme_name,omitempty"` } diff --git a/pkg/jms-sdk-go/model/user.go b/pkg/jms-sdk-go/model/user.go index 5ca3d9098..4f954ebdd 100644 --- a/pkg/jms-sdk-go/model/user.go +++ b/pkg/jms-sdk-go/model/user.go @@ -25,11 +25,9 @@ func (u *User) String() string { return fmt.Sprintf("%s(%s)", u.Name, u.Username) } -type TokenUser struct { - UserID string `json:"user"` - UserName string `json:"username"` - AssetID string `json:"asset"` - Hostname string `json:"hostname"` - SystemUserID string `json:"system_user"` - SystemUserName string `json:"system_user_name"` +type UserKokoPreference struct { + Basic KokoBasic `json:"basic"` +} +type KokoBasic struct { + ThemeName string `json:"terminal_theme_name"` } diff --git a/pkg/jms-sdk-go/service/jms_session.go b/pkg/jms-sdk-go/service/jms_session.go index 46cde7a41..982eb58c4 100644 --- a/pkg/jms-sdk-go/service/jms_session.go +++ b/pkg/jms-sdk-go/service/jms_session.go @@ -2,6 +2,7 @@ package service import ( "fmt" + "github.com/jumpserver/koko/pkg/jms-sdk-go/common" "github.com/jumpserver/koko/pkg/jms-sdk-go/model" ) @@ -46,11 +47,20 @@ func (s *JMService) SessionSuccess(sid string) error { } func (s *JMService) SessionFailed(sid string, err error) error { - data := map[string]bool{ - "is_success": false, + data := map[string]interface{}{ + "is_success": false, + "error_reason": model.SessionReplayErrConnectFailed, } return s.sessionPatch(sid, data) } + +func (s *JMService) SessionReplayFailed(sid string, err model.ReplayError) error { + data := map[string]interface{}{ + "error_reason": err, + } + return s.sessionPatch(sid, data) +} + func (s *JMService) SessionDisconnect(sid string) error { return s.SessionFinished(sid, common.NewNowUTCTime()) } diff --git a/pkg/jms-sdk-go/service/jms_share.go b/pkg/jms-sdk-go/service/jms_share.go index ecee1c1e9..a94811dc9 100644 --- a/pkg/jms-sdk-go/service/jms_share.go +++ b/pkg/jms-sdk-go/service/jms_share.go @@ -2,6 +2,8 @@ package service import ( "fmt" + "strings" + "github.com/jumpserver/koko/pkg/jms-sdk-go/model" ) @@ -28,3 +30,36 @@ func (s *JMService) FinishShareRoom(recordId string) (err error) { _, err = s.authClient.Patch(reqUrl, nil, nil) return } + +func (s *JMService) SyncUserKokoPreference(cookies map[string]string, data model.UserKokoPreference) (err error) { + /* + csrfToken 存储在 cookies 中 + 其 使用的名称 name 为 `{SESSION_COOKIE_NAME_PREFIX}csrftoken` 动态组成 + */ + var ( + csrfToken string + namePrefix string + ) + checkNamePrefixValid := func(name string) bool { + invalidStrings := []string{`""`, `''`} + for _, invalidString := range invalidStrings { + if strings.Contains(name, invalidString) { + return false + } + } + return true + } + namePrefix = cookies["SESSION_COOKIE_NAME_PREFIX"] + csrfCookieName := "csrftoken" + if namePrefix != "" && checkNamePrefixValid(namePrefix) { + csrfCookieName = namePrefix + csrfCookieName + } + csrfToken = cookies[csrfCookieName] + client := s.authClient.Clone() + client.SetHeader("X-CSRFToken", csrfToken) + for k, v := range cookies { + client.SetCookie(k, v) + } + _, err = client.Patch(UserKoKoPreferenceURL, data, nil) + return +} diff --git a/pkg/jms-sdk-go/service/jms_token.go b/pkg/jms-sdk-go/service/jms_token.go index 768f677c8..8c44ca344 100644 --- a/pkg/jms-sdk-go/service/jms_token.go +++ b/pkg/jms-sdk-go/service/jms_token.go @@ -2,16 +2,11 @@ package service import ( "fmt" + "strings" "github.com/jumpserver/koko/pkg/jms-sdk-go/model" ) -func (s *JMService) GetTokenAsset(token string) (tokenUser model.TokenUser, err error) { - Url := fmt.Sprintf(TokenAssetURL, token) - _, err = s.authClient.Get(Url, &tokenUser) - return -} - func (s *JMService) GetConnectTokenInfo(tokenId string) (resp model.ConnectToken, err error) { data := map[string]string{ "id": tokenId, @@ -21,20 +16,22 @@ func (s *JMService) GetConnectTokenInfo(tokenId string) (resp model.ConnectToken } func (s *JMService) CreateSuperConnectToken(data *SuperConnectTokenReq) (resp model.ConnectTokenInfo, err error) { - _, err = s.authClient.Post(SuperConnectTokenInfoURL, data, &resp, data.Params) - return -} - -func (s *JMService) CreateConnectTokenAndGetAuthInfo(params *SuperConnectTokenReq) (model.ConnectToken, error) { - tokenInfo, err := s.CreateSuperConnectToken(params) - if err != nil { - return model.ConnectToken{}, err + ak := s.opt.accessKey + apiClient := s.authClient.Clone() + if s.opt.sign != nil { + apiClient.SetAuthSign(s.opt.sign) } - connectToken, err := s.GetConnectTokenInfo(tokenInfo.ID) + apiClient.SetHeader(orgHeaderKey, orgHeaderValue) + // 移除 Secret 中的 "-", 保证长度为 32 + secretKey := strings.ReplaceAll(ak.Secret, "-", "") + encryptKey, err1 := GenerateEncryptKey(secretKey) if err != nil { - return model.ConnectToken{}, err + return resp, err1 } - return connectToken, nil + signKey := fmt.Sprintf("%s:%s", ak.ID, encryptKey) + apiClient.SetHeader(svcHeader, fmt.Sprintf("Sign %s", signKey)) + _, err = apiClient.Post(SuperConnectTokenInfoURL, data, &resp, data.Params) + return } type SuperConnectTokenReq struct { @@ -45,6 +42,7 @@ type SuperConnectTokenReq struct { ConnectMethod string `json:"connect_method"` InputUsername string `json:"input_username"` InputSecret string `json:"input_secret"` + RemoteAddr string `json:"remote_addr"` Params map[string]string `json:"-"` } diff --git a/pkg/jms-sdk-go/service/options.go b/pkg/jms-sdk-go/service/options.go index fa2b14b0f..917bfa8e5 100644 --- a/pkg/jms-sdk-go/service/options.go +++ b/pkg/jms-sdk-go/service/options.go @@ -4,13 +4,15 @@ import ( "time" "github.com/jumpserver/koko/pkg/jms-sdk-go/httplib" + "github.com/jumpserver/koko/pkg/jms-sdk-go/model" ) type option struct { // default http://127.0.0.1:8080 - CoreHost string - TimeOut time.Duration - sign httplib.AuthSign + CoreHost string + TimeOut time.Duration + sign httplib.AuthSign + accessKey model.AccessKey } type Option func(*option) @@ -33,5 +35,9 @@ func JMSAccessKey(keyID, secretID string) Option { KeyID: keyID, SecretID: secretID, } + o.accessKey = model.AccessKey{ + ID: keyID, + Secret: secretID, + } } } diff --git a/pkg/jms-sdk-go/service/url.go b/pkg/jms-sdk-go/service/url.go index f86d47f08..9b63abde7 100644 --- a/pkg/jms-sdk-go/service/url.go +++ b/pkg/jms-sdk-go/service/url.go @@ -77,3 +77,7 @@ const ( AssetLoginConfirmURL = "/api/v1/acls/login-asset/check/" AclCommandReviewURL = "/api/v1/acls/command-filter-acls/command-review/" ) + +const ( + UserKoKoPreferenceURL = "/api/v1/users/preference/?category=koko" +) diff --git a/pkg/jms-sdk-go/service/user_client.go b/pkg/jms-sdk-go/service/user_client.go index 3fca8639f..6958689e2 100644 --- a/pkg/jms-sdk-go/service/user_client.go +++ b/pkg/jms-sdk-go/service/user_client.go @@ -1,6 +1,14 @@ package service import ( + "bytes" + "crypto/aes" + "encoding/base64" + "fmt" + "strconv" + "strings" + "time" + "github.com/jumpserver/koko/pkg/jms-sdk-go/httplib" "github.com/jumpserver/koko/pkg/jms-sdk-go/model" ) @@ -33,6 +41,10 @@ func (u *UserClient) SetOption(setters ...UserClientOption) { } } +const ( + svcHeader = "X-JMS-SVC" +) + func (u *UserClient) GetAPIToken() (resp AuthResponse, err error) { data := map[string]string{ "username": u.Opts.Username, @@ -41,6 +53,15 @@ func (u *UserClient) GetAPIToken() (resp AuthResponse, err error) { "remote_addr": u.Opts.RemoteAddr, "login_type": u.Opts.LoginType, } + ak := u.Opts.signKey + // 移除 Secret 中的 "-", 保证长度为 32 + secretKey := strings.ReplaceAll(ak.Secret, "-", "") + encryptKey, err1 := GenerateEncryptKey(secretKey) + if err != nil { + return resp, err1 + } + signKey := fmt.Sprintf("%s:%s", ak.ID, encryptKey) + u.client.SetHeader(svcHeader, fmt.Sprintf("Sign %s", signKey)) _, err = u.client.Post(UserTokenAuthURL, data, &resp) return } @@ -129,6 +150,18 @@ func UserClientHttpClient(con *httplib.Client) UserClientOption { } } +func UserClientSvcSignKey(key model.AccessKey) UserClientOption { + return func(args *UserClientOptions) { + args.signKey = key + } +} + +func GenerateEncryptKey(key string) (string, error) { + seconds := time.Now().Unix() + value := strconv.FormatUint(uint64(seconds), 10) + return EncryptECB(value, key) +} + type UserClientOptions struct { Username string Password string @@ -136,4 +169,51 @@ type UserClientOptions struct { RemoteAddr string LoginType string client *httplib.Client + + signKey model.AccessKey +} + +func EncryptECB(plaintext string, key string) (string, error) { + block, err := aes.NewCipher([]byte(key)) + if err != nil { + return "", err + } + newPlaintext := make([]byte, 0, len(plaintext)) + newPlaintext = append(newPlaintext, []byte(plaintext)...) + if len(newPlaintext)%aes.BlockSize != 0 { + padding := aes.BlockSize - len(plaintext)%aes.BlockSize + newPlaintext = append(newPlaintext, bytes.Repeat([]byte{byte(0x00)}, padding)...) + } + + ciphertext := make([]byte, len(newPlaintext)) + for i := 0; i < len(newPlaintext); i += aes.BlockSize { + block.Encrypt(ciphertext[i:i+aes.BlockSize], newPlaintext[i:i+aes.BlockSize]) + } + ret := base64.StdEncoding.EncodeToString(ciphertext) + return ret, nil +} + +func DecryptECB(ciphertext string, key string) (string, error) { + ret, err := base64.StdEncoding.DecodeString(ciphertext) + if err != nil { + return "", err + } + block, err := aes.NewCipher([]byte(key)) + if err != nil { + return "", err + } + + if len(ret)%aes.BlockSize != 0 { + return "", fmt.Errorf("ciphertext is not a multiple of the block size") + } + plaintext := make([]byte, len(ret)) + for i := 0; i < len(ret); i += aes.BlockSize { + block.Decrypt(plaintext[i:i+aes.BlockSize], ret[i:i+aes.BlockSize]) + } + + // 移除 Zero 填充 + for len(plaintext) > 0 && plaintext[len(plaintext)-1] == 0x00 { + plaintext = plaintext[:len(plaintext)-1] + } + return string(plaintext), nil } diff --git a/pkg/koko/task.go b/pkg/koko/task.go index 52d557a5a..dd21178d7 100644 --- a/pkg/koko/task.go +++ b/pkg/koko/task.go @@ -68,6 +68,10 @@ func uploadRemainReplay(jmsService *service.JMService) { logger.Infof("Upload replay file: %s, type: %s", absGzPath, replayStorage.TypeName()) if err2 := replayStorage.Upload(absGzPath, Target); err2 != nil { logger.Errorf("Upload remain replay file %s failed: %s", absGzPath, err2) + reason := model.SessionReplayErrUploadFailed + if err3 := jmsService.SessionReplayFailed(remainReplay.Id, reason); err3 != nil { + logger.Errorf("Update session %s status %s failed: %s", remainReplay.Id, reason, err3) + } continue } if err := jmsService.FinishReply(remainReplay.Id); err != nil { diff --git a/pkg/proxy/parser.go b/pkg/proxy/parser.go index 40545d7c3..250b932c2 100644 --- a/pkg/proxy/parser.go +++ b/pkg/proxy/parser.go @@ -34,6 +34,10 @@ var ( []byte("\x1b[?1047l"), []byte("\x1b[?47l"), } + screenMarks = [][]byte{ + []byte{0x1b, 0x5b, 0x4b, 0x0d, 0x0a}, + []byte{0x1b, 0x5b, 0x34, 0x6c}, + } ) const ( @@ -452,8 +456,10 @@ func (p *Parser) parseZmodemState(b []byte) { // parseVimState 解析vim的状态,处于vim状态中,里面输入的命令不再记录 func (p *Parser) parseVimState(b []byte) { if !p.inVimState && IsEditEnterMode(b) { - p.inVimState = true - logger.Debug("In vim state: true") + if !isNewScreen(b) { + p.inVimState = true + logger.Debug("In vim state: true") + } } if p.inVimState && IsEditExitMode(b) { p.inVimState = false @@ -704,6 +710,10 @@ type CurrentActiveUser struct { RemoteAddr string } +func isNewScreen(p []byte) bool { + return matchMark(p, screenMarks) +} + func IsEditEnterMode(p []byte) bool { return matchMark(p, enterMarks) } diff --git a/pkg/proxy/recorder.go b/pkg/proxy/recorder.go index 659d66bf8..35b48e822 100644 --- a/pkg/proxy/recorder.go +++ b/pkg/proxy/recorder.go @@ -145,6 +145,10 @@ func NewReplayRecord(sid string, jmsService *service.JMService, fd, err := os.Create(recorder.absFilePath) if err != nil { logger.Errorf("Create replay file %s error: %s\n", recorder.absFilePath, err) + reason := model.SessionReplayErrCreatedFailed + if err1 := jmsService.SessionReplayFailed(sid, reason); err1 != nil { + logger.Errorf("Session[%s] update replay status %s failed: %s", sid, reason, err1) + } recorder.err = err return recorder, err } @@ -242,6 +246,10 @@ func (r *ReplyRecorder) UploadGzipFile(maxRetry int) { // 如果还是失败,上传 server 再传一次 if i == maxRetry { if r.storage.TypeName() == "server" { + reason := model.SessionReplayErrUploadFailed + if err1 := r.jmsService.SessionReplayFailed(r.SessionID, reason); err1 != nil { + logger.Errorf("Session[%s] update replay status %s failed: %s", r.SessionID, reason, err1) + } break } logger.Errorf("Session[%s] using server storage retry upload", r.SessionID) diff --git a/pkg/proxy/server.go b/pkg/proxy/server.go index 3d8e54569..7e91d0c1f 100644 --- a/pkg/proxy/server.go +++ b/pkg/proxy/server.go @@ -165,6 +165,8 @@ type SessionInfo struct { BackspaceAsCtrlH *bool `json:"backspaceAsCtrlH,omitempty"` CtrlCAsCtrlZ bool `json:"ctrlCAsCtrlZ"` + + ThemeName string `json:"themeName"` } func (s *Server) IsKeyboardMode() bool { @@ -1091,6 +1093,7 @@ func (s *Server) Proxy() { BackspaceAsCtrlH: tokenConnOpts.BackspaceAsCtrlH, CtrlCAsCtrlZ: ctrlCAsCtrlZ, + ThemeName: tokenConnOpts.TerminalThemeName, } go s.OnSessionInfo(&info) } diff --git a/pkg/proxy/server_options.go b/pkg/proxy/server_options.go index b7eeb7944..6736d219d 100644 --- a/pkg/proxy/server_options.go +++ b/pkg/proxy/server_options.go @@ -112,7 +112,7 @@ func (opts *ConnectionOptions) ConnectMsg() string { } msg = fmt.Sprintf(lang.T("Connecting to %s@%s"), accountName, asset.Address) case srvconn.ProtocolClickHouse, - srvconn.ProtocolRedis, srvconn.ProtocolMongoDB, + srvconn.ProtocolRedis, srvconn.ProtocolMongoDB, srvconn.ProtocolMariadb, srvconn.ProtocolMySQL, srvconn.ProtocolSQLServer, srvconn.ProtocolPostgresql: msg = fmt.Sprintf(lang.T("Connecting to Database %s"), asset.String()) case srvconn.ProtocolK8s: diff --git a/pkg/srvconn/conn_mongodb.go b/pkg/srvconn/conn_mongodb.go index 8937b821d..3f3e5b6cc 100644 --- a/pkg/srvconn/conn_mongodb.go +++ b/pkg/srvconn/conn_mongodb.go @@ -8,6 +8,7 @@ import ( "strconv" "time" + "github.com/jumpserver/koko/pkg/logger" "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" @@ -92,7 +93,12 @@ func (conn *MongoDBConn) Close() error { func startMongoDBCommand(opt *sqlOption) (lcmd *localcommand.LocalCommand, err error) { cmd := opt.MongoDBCommandArgs() - lcmd, err = localcommand.New("mongosh", cmd, localcommand.WithPtyWin(opt.win.Width, opt.win.Height)) + opts, err := BuildNobodyWithOpts(localcommand.WithPtyWin(opt.win.Width, opt.win.Height)) + if err != nil { + logger.Errorf("build nobody with opts error: %s", err) + return nil, err + } + lcmd, err = localcommand.New("mongosh", cmd, opts...) if err != nil { return nil, err } diff --git a/pkg/srvconn/conn_nobody.go b/pkg/srvconn/conn_nobody.go new file mode 100644 index 000000000..7b8b2ce03 --- /dev/null +++ b/pkg/srvconn/conn_nobody.go @@ -0,0 +1,24 @@ +package srvconn + +import ( + "os/user" + "strconv" + "syscall" + + "github.com/jumpserver/koko/pkg/localcommand" +) + +func BuildNobodyWithOpts(opts ...localcommand.Option) (nobodyOpts []localcommand.Option, err error) { + nobody, err := user.Lookup("nobody") + if err != nil { + return nil, err + } + uid, _ := strconv.Atoi(nobody.Uid) + gid, _ := strconv.Atoi(nobody.Gid) + nobodyOpts = make([]localcommand.Option, 0, len(opts)+1) + nobodyOpts = append(nobodyOpts, opts...) + nobodyCredential := localcommand.WithCmdCredential(&syscall.Credential{Uid: uint32(uid), Gid: uint32(gid)}) + nobodyOpts = append(nobodyOpts, localcommand.WithEnv(make([]string, 0))) + nobodyOpts = append(nobodyOpts, nobodyCredential) + return nobodyOpts, nil +} diff --git a/pkg/srvconn/conn_postgresql.go b/pkg/srvconn/conn_postgresql.go index 9a52d3d54..b3f406d1e 100644 --- a/pkg/srvconn/conn_postgresql.go +++ b/pkg/srvconn/conn_postgresql.go @@ -5,6 +5,7 @@ import ( "os" "strconv" + "github.com/jumpserver/koko/pkg/logger" _ "github.com/lib/pq" "github.com/jumpserver/koko/pkg/localcommand" @@ -61,7 +62,12 @@ func (conn *PostgreSQLConn) Close() error { func startPostgreSQLCommand(opt *sqlOption) (lcmd *localcommand.LocalCommand, err error) { argv := opt.PostgreSQLCommandArgs() //psql 是启动postgresql的客户端 - lcmd, err = localcommand.New("psql", argv, localcommand.WithPtyWin(opt.win.Width, opt.win.Height)) + opts, err := BuildNobodyWithOpts(localcommand.WithPtyWin(opt.win.Width, opt.win.Height)) + if err != nil { + logger.Errorf("build nobody with opts error: %s", err) + return nil, err + } + lcmd, err = localcommand.New("psql", argv, opts...) if err != nil { return nil, err } diff --git a/pkg/srvconn/conn_redis.go b/pkg/srvconn/conn_redis.go index a90db0eef..3bd37c3fb 100644 --- a/pkg/srvconn/conn_redis.go +++ b/pkg/srvconn/conn_redis.go @@ -9,6 +9,7 @@ import ( "time" "github.com/jumpserver/koko/pkg/localcommand" + "github.com/jumpserver/koko/pkg/logger" "github.com/mediocregopher/radix/v3" ) @@ -95,7 +96,12 @@ func (conn *RedisConn) Close() error { func startRedisCommand(opt *sqlOption) (lcmd *localcommand.LocalCommand, err error) { cmd := opt.RedisCommandArgs() - lcmd, err = localcommand.New("redis-cli", cmd, localcommand.WithPtyWin(opt.win.Width, opt.win.Height)) + opts, err := BuildNobodyWithOpts(localcommand.WithPtyWin(opt.win.Width, opt.win.Height)) + if err != nil { + logger.Errorf("build nobody with opts error: %s", err) + return nil, err + } + lcmd, err = localcommand.New("redis-cli", cmd, opts...) if err != nil { return nil, err } diff --git a/pkg/srvconn/conn_sqlserver.go b/pkg/srvconn/conn_sqlserver.go index 6e5c01bf1..02cf238bc 100644 --- a/pkg/srvconn/conn_sqlserver.go +++ b/pkg/srvconn/conn_sqlserver.go @@ -72,7 +72,12 @@ func startSQLServerCommand(opt *sqlOption) (lcmd *localcommand.LocalCommand, err func startSQLServerNormalCommand(opt *sqlOption) (lcmd *localcommand.LocalCommand, err error) { //tsql 是启动sqlserver的客户端 - return localcommand.New("tsql", opt.SQLServerCommandArgs()) + opts, err := BuildNobodyWithOpts(localcommand.WithPtyWin(opt.win.Width, opt.win.Height)) + if err != nil { + logger.Errorf("build nobody with opts error: %s", err) + return nil, err + } + return localcommand.New("tsql", opt.SQLServerCommandArgs(), opts...) } func tryManualLoginSQLServerServer(opt *sqlOption, lcmd *localcommand.LocalCommand) (*localcommand.LocalCommand, error) { diff --git a/pkg/srvconn/sftp_asset.go b/pkg/srvconn/sftp_asset.go index fce369963..68e12b31b 100644 --- a/pkg/srvconn/sftp_asset.go +++ b/pkg/srvconn/sftp_asset.go @@ -578,6 +578,7 @@ func (ad *AssetDir) createConnectToken(su *model.PermAccount) (model.ConnectToke Account: su.Alias, Protocol: model.ProtocolSFTP, ConnectMethod: model.ProtocolSFTP, + RemoteAddr: ad.opts.RemoteAddr, } // sftp 不支持 ACL 复核的资产,需要从 web terminal 中登录 tokenInfo, err := ad.jmsService.CreateSuperConnectToken(&req) diff --git a/pkg/sshd/server.go b/pkg/sshd/server.go index 6087932e5..a1338e81a 100644 --- a/pkg/sshd/server.go +++ b/pkg/sshd/server.go @@ -73,6 +73,7 @@ func NewSSHServer(jmsService *service.JMService) *Server { KeyboardInteractiveHandler: auth.SSHKeyboardInteractiveAuth, PasswordHandler: sshHandler.PasswordAuth, PublicKeyHandler: sshHandler.PublicKeyAuth, + AuthLogCallback: auth.SSHAuthLogCallback, NextAuthMethodsHandler: func(ctx ssh.Context) []string { return []string{nextAuthMethod} }, HostSigners: []ssh.Signer{singer}, ServerConfigCallback: func(ctx ssh.Context) *gossh.ServerConfig { diff --git a/pkg/utils/aes_test.go b/pkg/utils/aes_test.go index e344c5488..aec2226db 100644 --- a/pkg/utils/aes_test.go +++ b/pkg/utils/aes_test.go @@ -1,6 +1,9 @@ package utils import ( + "bytes" + "crypto/aes" + "encoding/base64" "testing" ) @@ -23,3 +26,34 @@ func TestDecrypt(t *testing.T) { } } + +func TestEncrypt(t *testing.T) { + secret := "4bd477efa46d4acea8016af7b332589d" + src := "abc" + ret, err := encryptECB([]byte(src), []byte(secret)) + if err != nil { + t.Fatal(err) + } + + t.Log(base64.StdEncoding.EncodeToString(ret)) + +} + +func encryptECB(plaintext []byte, key []byte) ([]byte, error) { + block, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + + if len(plaintext)%aes.BlockSize != 0 { + padding := aes.BlockSize - len(plaintext)%aes.BlockSize + plaintext = append(plaintext, bytes.Repeat([]byte{byte(0x00)}, padding)...) + } + + ciphertext := make([]byte, len(plaintext)) + for i := 0; i < len(plaintext); i += aes.BlockSize { + block.Encrypt(ciphertext[i:i+aes.BlockSize], plaintext[i:i+aes.BlockSize]) + } + + return ciphertext, nil +} diff --git a/ui/src/components/RightPanel.vue b/ui/src/components/RightPanel.vue index d121da356..976cef335 100644 --- a/ui/src/components/RightPanel.vue +++ b/ui/src/components/RightPanel.vue @@ -137,6 +137,16 @@ export default { z-index: 1200; } +.right-panel-items { + height: 100%; + overflow-y: auto; +} + +.right-panel-items::-webkit-scrollbar-track { + box-shadow: none; + background-color: transparent; +} + .show { transition: all .3s cubic-bezier(.7, .3, .1, 1); } diff --git a/ui/src/components/Terminal.vue b/ui/src/components/Terminal.vue index bad7c543b..aed036868 100644 --- a/ui/src/components/Terminal.vue +++ b/ui/src/components/Terminal.vue @@ -29,7 +29,7 @@ import 'xterm/css/xterm.css' import {Terminal} from 'xterm'; import {FitAddon} from 'xterm-addon-fit'; import ZmodemBrowser from "nora-zmodemjs/src/zmodem_browser"; -import {bytesHuman, fireEvent} from '@/utils/common' +import {bytesHuman, defaultTheme, fireEvent} from '@/utils/common' import xtermTheme from "xterm-theme"; const MaxTimeout = 30 * 1000 @@ -52,7 +52,11 @@ export default { enableZmodem: { type: Boolean, default: false, - } + }, + themeName: { + type: String, + default: 'Default', + }, }, data() { return { @@ -80,17 +84,14 @@ export default { mounted: function () { this.registerJMSEvent() this.connect() - this.updateTheme() + this.updateTheme(this.themeName) }, methods: { - updateTheme() { - const ThemeName = window.localStorage.getItem("themeName") || null - if (ThemeName) { - const theme = xtermTheme[ThemeName] - this.term.setOption("theme", theme); - this.$log.debug("theme: ", ThemeName) - this.$emit("background-color", theme.background) - } + updateTheme(themeName) { + const theme = xtermTheme[themeName] || defaultTheme + this.term.setOption("theme", theme) + this.$log.debug("theme: ",themeName) + this.$emit("background-color", theme.background) }, createTerminal() { let lineHeight = this.config.lineHeight; @@ -403,6 +404,26 @@ export default { this.term.writeln(errMsg); break } + case 'MESSAGE_NOTIFY': { + const errMsg = msg.err; + const eventData = JSON.parse(msg.data); + + const eventName = eventData.event_name; + switch (eventName) { + case 'sync_user_preference': + if (errMsg === '' || errMsg === null) { + const successNotify = this.$t("Terminal.SyncUserPreferenceSuccess") + this.$message.success(successNotify) + } else { + const errNotify = `${this.$t("Terminal.SyncUserPreferenceFailed")}: ${errMsg}` + this.$message.error(errNotify); + } + break + default: + this.$log.debug("unknown: ", eventName) + } + break + } default: this.$log.debug("default: ", data) } @@ -636,6 +657,10 @@ export default { this.sendWsMessage('TERMINAL_SHARE', data) }, + syncUserPreference(data) { + this.sendWsMessage('TERMINAL_SYNC_USER_PREFERENCE', data) + }, + removeShareUser(sessionId, userMeta) { this.sendWsMessage('TERMINAL_SHARE_USER_REMOVE', {session:sessionId, user_meta:userMeta}) }, diff --git a/ui/src/components/ThemeConfig.vue b/ui/src/components/ThemeConfig.vue index eb319ce3d..4a449134c 100644 --- a/ui/src/components/ThemeConfig.vue +++ b/ui/src/components/ThemeConfig.vue @@ -8,9 +8,16 @@ :close-on-press-escape="false">
- - - + + + + + + + + {{ this.$t('Terminal.Sync') }} + +

Theme Colors

@@ -110,12 +117,17 @@ const themes = Object.keys(xtermTheme); export default { name: "ThemeConfig", props: { - visible: Boolean + visible: Boolean, + themeName: { + type: String, + required: true + }, }, data() { return { themes: ['Default', ...themes], - theme: window.localStorage.getItem("themeName") || 'Default', + theme: 'Default', + loading: false, }; }, computed: { @@ -138,12 +150,22 @@ export default { watch: { theme(val) { const theme = val && val !== 'Default' ? val : ''; - window.localStorage.setItem("themeName", theme); - this.$emit("setTheme", xtermTheme[theme]); + this.$emit("setTheme",theme, xtermTheme[theme]); + }, + themeName(val) { + this.theme = val; } }, - mounted() { - this.$emit("setTheme", xtermTheme[this.theme]); + methods: { + syncTheme() { + this.loading = true; + const vm = this; + this.$emit("syncThemeName",this.theme, xtermTheme[this.theme]); + // 5s后关闭loading, 避免出现异常 + setTimeout(function () { + vm.loading = false; + }, 1000*5); + } } }; @@ -177,4 +199,8 @@ export default { height: 300px; } +.sync-btn { + background-color: #343333; + color: white; +} \ No newline at end of file diff --git a/ui/src/i18n/langs/en.json b/ui/src/i18n/langs/en.json index 34644f8c5..89e25e22e 100644 --- a/ui/src/i18n/langs/en.json +++ b/ui/src/i18n/langs/en.json @@ -45,7 +45,10 @@ "Minute": "Minute", "Minutes": "Minutes", "PauseSession" : "Pause Session", - "ResumeSession" : "Resume Session" + "ResumeSession" : "Resume Session", + "SyncUserPreferenceSuccess": "Sync user preference success", + "SyncUserPreferenceFailed": "Sync user preference failed", + "Sync": "Sync" }, "Message": { "InputVerifyCode": "Input Verify Code" diff --git a/ui/src/i18n/langs/ja.json b/ui/src/i18n/langs/ja.json index 7d064b88a..e41fc9151 100644 --- a/ui/src/i18n/langs/ja.json +++ b/ui/src/i18n/langs/ja.json @@ -45,7 +45,10 @@ "Minute": "分間", "Minutes": "分間", "PauseSession" : "セッションを一時停止", - "ResumeSession" : "セッションを再開" + "ResumeSession" : "セッションを再開", + "SyncUserPreferenceSuccess": "ユーザー設定の同期に成功しました", + "SyncUserPreferenceFailed": "ユーザー設定の同期に失敗しました", + "Sync": "同期" }, "Message": { "InputVerifyCode": "認証コードを入力してください" diff --git a/ui/src/i18n/langs/zh.json b/ui/src/i18n/langs/zh.json index 052c8dbd2..13f882e00 100644 --- a/ui/src/i18n/langs/zh.json +++ b/ui/src/i18n/langs/zh.json @@ -45,7 +45,10 @@ "Minute": "分钟", "Minutes": "分钟", "PauseSession" : "暂停此会话", - "ResumeSession" : "恢复此会话" + "ResumeSession" : "恢复此会话", + "SyncUserPreferenceSuccess": "同步设置成功", + "SyncUserPreferenceFailed": "同步设置失败", + "Sync": "同步" }, "Message": { "InputVerifyCode": "请输入验证码" diff --git a/ui/src/utils/common.js b/ui/src/utils/common.js index d22d36e9d..acb955164 100644 --- a/ui/src/utils/common.js +++ b/ui/src/utils/common.js @@ -144,25 +144,25 @@ export function removeClass(ele, cls) { /** * Default theme + * copy from https://github.com/xtermjs/xterm.js/blob/master/src/browser/services/ThemeService.ts#L152 */ export const defaultTheme = { background: '#1f1b1b', - black: '#151515', - blue: '#6c99bb', - brightBlack: '#505050', - brightBlue: '#6c99bb', - brightCyan: '#7dd6cf', - brightGreen: '#7e8e50', - brightMagenta: '#9f4e85', - brightRed: '#ac4142', - brightWhite: '#f5f5f5', - brightYellow: '#e5b567', - cursor: '#d0d0d0', - cyan: '#7dd6cf', - foreground: '#d0d0d0', - green: '#7e8e50', - magenta: '#9f4e85', - red: '#ac4142', - white: '#d0d0d0', - yellow: '#e5b567' -} + foreground: '#ffffff', + black:'#2e3436', + red:'#cc0000', + green:'#4e9a06', + yellow:'#c4a000', + blue: '#3465a4', + magenta: '#75507b', + cyan:'#06989a', + white:'#d3d7cf', + brightBlack:'#555753', + brightRed:'#ef2929', + brightGreen:'#8ae234', + brightYellow:'#fce94f', + brightBlue:'#729fcf', + brightMagenta:'#ad7fa8', + brightCyan:'#34e2e2', + brightWhite: '#eeeeec', +} \ No newline at end of file diff --git a/ui/src/views/Connection.vue b/ui/src/views/Connection.vue index 3ec995ecf..48dceffd4 100644 --- a/ui/src/views/Connection.vue +++ b/ui/src/views/Connection.vue @@ -12,7 +12,10 @@ - + +