diff --git a/.gitignore b/.gitignore index 8b31747..c88c7e0 100644 --- a/.gitignore +++ b/.gitignore @@ -43,6 +43,5 @@ ui/pnpm-debug.log* *.njsproj *.sln *.sw? -ui/yarn.lock build -package-lock.json +build/ diff --git a/Dockerfile b/Dockerfile index 55dbf6b..15cb1d8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -44,8 +44,10 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=lion \ && ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \ && apt-get update \ && apt-get install -y --no-install-recommends ${DEPENDENCIES} \ + && apt-get clean \ && sed -i "s@# export @export @g" ~/.bashrc \ - && sed -i "s@# alias @alias @g" ~/.bashrc + && sed -i "s@# alias @alias @g" ~/.bashrc \ + && mkdir -p /lib32 /libx32 WORKDIR /opt/lion @@ -67,4 +69,4 @@ EXPOSE 8081 STOPSIGNAL SIGQUIT -CMD [ "supervisord", "-c", "/etc/supervisor/supervisord.conf" ] \ No newline at end of file +CMD [ "supervisord", "-c", "/etc/supervisor/supervisord.conf" ] diff --git a/main.go b/main.go index 7ae07a0..bd9a8c3 100644 --- a/main.go +++ b/main.go @@ -76,6 +76,7 @@ func main() { eng := registerRouter(jmsService, &tunnelService) go runHeartTask(jmsService, tunnelService.Cache) go runCleanDriverDisk(tunnelService.Cache) + go runTokenCheck(jmsService, tunnelService.Cache) addr := net.JoinHostPort(config.GlobalConfig.BindHost, config.GlobalConfig.HTTPPort) fmt.Printf("Lion Version %s, more see https://www.jumpserver.org\n", Version) logger.Infof("listen on: %s", addr) @@ -212,6 +213,48 @@ func runCleanDriverDisk(tunnelCache *tunnel.GuaTunnelCacheManager) { } } +func runTokenCheck(jmsService *service.JMService, tunnelCache *tunnel.GuaTunnelCacheManager) { + for { + time.Sleep(5 * time.Minute) + connections := tunnelCache.GetActiveConnections() + tokens := make(map[string]model.TokenCheckStatus, len(connections)) + for _, s := range connections { + tokenId := s.Sess.AuthInfo.Id + ret, ok := tokens[tokenId] + if ok { + handleTokenCheck(s, &ret) + continue + } + ret, err := jmsService.CheckTokenStatus(tokenId) + if err != nil && ret.Code == "" { + logger.Errorf("Check token status failed: %s", err) + continue + } + tokens[tokenId] = ret + handleTokenCheck(s, &ret) + } + } +} + +func handleTokenCheck(session *tunnel.Connection, tokenStatus *model.TokenCheckStatus) { + var task model.TerminalTask + switch tokenStatus.Code { + case model.CodePermOk: + task = model.TerminalTask{ + Name: model.TaskPermValid, + Args: tokenStatus.Detail, + } + default: + task = model.TerminalTask{ + Name: model.TaskPermExpired, + Args: tokenStatus.Detail, + } + } + if err := session.HandleTask(&task); err != nil { + logger.Errorf("Handle token check task failed: %s", err) + } +} + func registerRouter(jmsService *service.JMService, tunnelService *tunnel.GuacamoleTunnelServer) *gin.Engine { if config.GlobalConfig.LogLevel != "DEBUG" { gin.SetMode(gin.ReleaseMode) @@ -232,7 +275,7 @@ func registerRouter(jmsService *service.JMService, tunnelService *tunnel.Guacamo lionGroup.GET("/health/", func(ctx *gin.Context) { status := make(map[string]interface{}) status["timestamp"] = time.Now().UTC() - status["uptime"] = time.Now().Sub(now).Minutes() + status["uptime"] = time.Since(now).Minutes() ctx.JSON(http.StatusOK, status) }) } diff --git a/pkg/common/random.go b/pkg/common/random.go index 99333d5..e6240b1 100644 --- a/pkg/common/random.go +++ b/pkg/common/random.go @@ -7,8 +7,10 @@ import ( const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" +var localRandom = rand.New(rand.NewSource(time.Now().UnixNano())) + func RandomStr(length int) string { - rand.Seed(time.Now().UnixNano()) + localRandom.Seed(time.Now().UnixNano()) b := make([]byte, length) for i := range b { b[i] = letters[rand.Intn(len(letters))] diff --git a/pkg/jms-sdk-go/httplib/client.go b/pkg/jms-sdk-go/httplib/client.go index 4be9813..0f80443 100644 --- a/pkg/jms-sdk-go/httplib/client.go +++ b/pkg/jms-sdk-go/httplib/client.go @@ -7,7 +7,6 @@ import ( "errors" "fmt" "io" - "io/ioutil" "mime/multipart" "net/http" "net/url" @@ -180,7 +179,7 @@ func (c *Client) Do(method, reqUrl string, data, res interface{}, params ...map[ return } defer resp.Body.Close() - body, err := ioutil.ReadAll(resp.Body) + body, err := io.ReadAll(resp.Body) if err != nil { return resp, err } diff --git a/pkg/jms-sdk-go/model/asset.go b/pkg/jms-sdk-go/model/asset.go index abb414d..54ee76d 100644 --- a/pkg/jms-sdk-go/model/asset.go +++ b/pkg/jms-sdk-go/model/asset.go @@ -89,7 +89,7 @@ type Protocols []Protocol func (p Protocols) GetProtocolPort(protocol string) int { for i := range p { - if strings.ToLower(p[i].Name) == strings.ToLower(protocol) { + if strings.EqualFold(p[i].Name, protocol) { return p[i].Port } } diff --git a/pkg/jms-sdk-go/model/terminal.go b/pkg/jms-sdk-go/model/terminal.go index a71ceb7..ceb068f 100644 --- a/pkg/jms-sdk-go/model/terminal.go +++ b/pkg/jms-sdk-go/model/terminal.go @@ -35,6 +35,11 @@ const ( TaskLockSession = "lock_session" TaskUnlockSession = "unlock_session" + + // TaskPermExpired TaskPermValid 非 api 数据,仅用于内部处理 + + TaskPermExpired = "perm_expired" + TaskPermValid = "perm_valid" ) type TaskKwargs struct { diff --git a/pkg/jms-sdk-go/model/token.go b/pkg/jms-sdk-go/model/token.go index 7ffb93a..5e3060d 100644 --- a/pkg/jms-sdk-go/model/token.go +++ b/pkg/jms-sdk-go/model/token.go @@ -44,3 +44,17 @@ type ConnectOptions struct { Resolution string `json:"resolution"` BackspaceAsCtrlH bool `json:"backspaceAsCtrlH"` } + +// token 授权和过期状态 + +type TokenCheckStatus struct { + Detail string `json:"detail"` + Code string `json:"code"` + Expired bool `json:"expired"` +} + +const ( + CodePermOk = "perm_ok" + CodePermAccountInvalid = "perm_account_invalid" + CodePermExpired = "perm_expired" +) diff --git a/pkg/jms-sdk-go/service/jms.go b/pkg/jms-sdk-go/service/jms.go index 66a10cf..ee61ce3 100644 --- a/pkg/jms-sdk-go/service/jms.go +++ b/pkg/jms-sdk-go/service/jms.go @@ -83,7 +83,7 @@ func (s *JMService) GetWsClient() (*websocket.Conn, error) { if err != nil { return nil, err } - scheme := "ws" + var scheme string switch u.Scheme { case "http": scheme = "ws" diff --git a/pkg/jms-sdk-go/service/jms_token.go b/pkg/jms-sdk-go/service/jms_token.go index bdf1dcc..54bceef 100644 --- a/pkg/jms-sdk-go/service/jms_token.go +++ b/pkg/jms-sdk-go/service/jms_token.go @@ -44,3 +44,9 @@ func (s *JMService) GetConnectTokenVirtualAppOption(tokenId string) (resp model. _, err = s.authClient.Post(SuperConnectTokenVirtualAppOptionURL, data, &resp) return } + +func (s *JMService) CheckTokenStatus(tokenId string) (res model.TokenCheckStatus, err error) { + reqURL := fmt.Sprintf(SuperConnectTokenCheckURL, tokenId) + _, err = s.authClient.Get(reqURL, &res) + return +} diff --git a/pkg/jms-sdk-go/service/panda/client.go b/pkg/jms-sdk-go/service/panda/client.go index 9e583a5..2f7d696 100644 --- a/pkg/jms-sdk-go/service/panda/client.go +++ b/pkg/jms-sdk-go/service/panda/client.go @@ -32,7 +32,6 @@ func NewClient(baseUrl string, key model.AccessKey, insecure bool) *Client { type Client struct { BaseURL string - sign httplib.AuthSign client *httplib.Client } diff --git a/pkg/jms-sdk-go/service/panda/client_test.go b/pkg/jms-sdk-go/service/panda/client_test.go deleted file mode 100644 index 3767e5a..0000000 --- a/pkg/jms-sdk-go/service/panda/client_test.go +++ /dev/null @@ -1,26 +0,0 @@ -package panda - -import ( - "testing" - - "lion/pkg/jms-sdk-go/model" -) - -func TestNewClient(t *testing.T) { - key := model.AccessKey{ - ID: "8298f537-12c2-4c7c-aac1-5330b5f46a46", - Secret: "6323d771-e714-44ca-a1e8-d4771978913d", - //Secret: "6323d771-e714-44ca-a1e8-d4771978913ds", - } - client := NewClient("http://localhost:9001", key, false) - if client == nil { - t.Error("client is nil") - } - token := "asdasdsdaasd" - ret, err := client.CreateContainer(token, model.VirtualAppOption{}) - if err != nil { - t.Error(err) - } - t.Logf("%+v\n", ret) - err = client.ReleaseContainer("asdasdasd") -} diff --git a/pkg/jms-sdk-go/service/url.go b/pkg/jms-sdk-go/service/url.go index 662a2e2..8b14dd3 100644 --- a/pkg/jms-sdk-go/service/url.go +++ b/pkg/jms-sdk-go/service/url.go @@ -48,6 +48,8 @@ const ( SuperConnectAppletHostAccountReleaseURL = "/api/v1/authentication/super-connection-token/applet-account/release/" SuperConnectTokenVirtualAppOptionURL = "/api/v1/authentication/super-connection-token/virtual-app-option/" + + SuperConnectTokenCheckURL = "/api/v1/authentication/super-connection-token/%s/check/" ) const ( diff --git a/pkg/jms-sdk-go/service/videoworker/video_worker.go b/pkg/jms-sdk-go/service/videoworker/video_worker.go index 7a7a2bd..d6e9db7 100644 --- a/pkg/jms-sdk-go/service/videoworker/video_worker.go +++ b/pkg/jms-sdk-go/service/videoworker/video_worker.go @@ -35,10 +35,7 @@ func NewClient(baseUrl string, key model.AccessKey, Insecure bool) *Client { type Client struct { BaseURL string - sign httplib.AuthSign client *httplib.Client - - cacheToken map[string]interface{} } func (s *Client) CreateReplayTask(sessionId string, file string, meta ReplayMeta) (model.Task, error) { @@ -86,21 +83,21 @@ func StructToMapString(m interface{}) map[string]string { if tagValue := fi.Tag.Get(tagName); tagValue != "" { interValue := v.Field(i).Interface() fieldValue := "" - switch interValue.(type) { + switch interValue1 := interValue.(type) { case string: - fieldValue = interValue.(string) + fieldValue = interValue1 case int: - fieldValue = strconv.Itoa(interValue.(int)) + fieldValue = strconv.Itoa(interValue1) case int32: - fieldValue = strconv.FormatInt(int64(interValue.(int32)), 10) + fieldValue = strconv.FormatInt(int64(interValue1), 10) case int64: - fieldValue = strconv.FormatInt(interValue.(int64), 10) + fieldValue = strconv.FormatInt(interValue1, 10) case float64: - fieldValue = strconv.FormatFloat(interValue.(float64), 'f', -1, 64) + fieldValue = strconv.FormatFloat(interValue1, 'f', -1, 64) case bool: - fieldValue = strconv.FormatBool(interValue.(bool)) + fieldValue = strconv.FormatBool(interValue1) default: - fieldValue = fmt.Sprintf("%v", interValue) + fieldValue = fmt.Sprintf("%v", interValue1) } // 如果值为空或者为0则不传递 if fieldValue == "" || fieldValue == "0" { diff --git a/pkg/session/parser.go b/pkg/session/parser.go index 03c6d2f..7e97cac 100644 --- a/pkg/session/parser.go +++ b/pkg/session/parser.go @@ -110,8 +110,8 @@ func (p *Parser) ParseStream(userInChan chan *Message) { unicode := strconv.FormatInt(int64(keyCode), 16) bs, _ := hex.DecodeString(unicode[3:]) for i, bl, br, r := 0, len(bs), bytes.NewReader(bs), uint16(0); i < bl; i += 2 { - binary.Read(br, binary.BigEndian, &r) - to += string(r) + _ = binary.Read(br, binary.BigEndian, &r) + to += string(rune(r)) } b = append(b, []byte(to)...) } else { diff --git a/pkg/session/server.go b/pkg/session/server.go index 452c4e8..d0efd36 100644 --- a/pkg/session/server.go +++ b/pkg/session/server.go @@ -221,7 +221,7 @@ func (s *Server) Create(ctx *gin.Context, opts ...TunnelOption) (sess TunnelSess for _, setter := range opts { setter(opt) } - targetType := TypeRDP + var targetType string sessionProtocol := opt.Protocol switch opt.authInfo.ConnectMethod.Type { case connectApplet, connectVirtualAPP: diff --git a/pkg/storage/util.go b/pkg/storage/util.go index d7147ed..e9bc8f7 100644 --- a/pkg/storage/util.go +++ b/pkg/storage/util.go @@ -193,13 +193,13 @@ func ParseEndpointRegion(s string) string { } endpoint, err := url.Parse(s) if err != nil { - return "" + return s } endpoints := strings.Split(endpoint.Hostname(), ".") if len(endpoints) >= 3 { return endpoints[len(endpoints)-3] } - return "" + return endpoints[0] } func ParseAWSURLRegion(s string) string { diff --git a/pkg/tunnel/cache.go b/pkg/tunnel/cache.go index 5ebafe8..d7770ad 100644 --- a/pkg/tunnel/cache.go +++ b/pkg/tunnel/cache.go @@ -28,6 +28,8 @@ type GuaTunnelCache interface { GetSessionEventChan(sid string) *EventChan BroadcastSessionEvent(sid string, event *Event) RecycleSessionEventChannel(sid string, eventChan *EventChan) + + GetActiveConnections() []*Connection } type SessionEvent interface { @@ -117,4 +119,7 @@ const ( ShareRemoveUser = "share_remove_user" ShareSessionPause = "share_session_pause" ShareSessionResume = "share_session_resume" + + PermExpiredEvent = "perm_expired" + PermValidEvent = "perm_valid" ) diff --git a/pkg/tunnel/cache_local.go b/pkg/tunnel/cache_local.go index c11b30f..6c93a63 100644 --- a/pkg/tunnel/cache_local.go +++ b/pkg/tunnel/cache_local.go @@ -121,3 +121,13 @@ func (g *GuaTunnelLocalCache) RecycleSessionEventChannel(sid string, eventChan * } } } + +func (g *GuaTunnelLocalCache) GetActiveConnections() []*Connection { + g.Lock() + defer g.Unlock() + ret := make([]*Connection, 0, len(g.Tunnels)) + for i := range g.Tunnels { + ret = append(ret, g.Tunnels[i]) + } + return ret +} diff --git a/pkg/tunnel/cache_remote.go b/pkg/tunnel/cache_remote.go index fc69a83..7a62c60 100644 --- a/pkg/tunnel/cache_remote.go +++ b/pkg/tunnel/cache_remote.go @@ -148,9 +148,6 @@ type GuaTunnelRedisCache struct { redisProxyExitChan chan string redisConExitChan chan string - - roomLock sync.Mutex - remoteRooms map[string]*Room } func (r *GuaTunnelRedisCache) BroadcastSessionEvent(sid string, event *Event) { diff --git a/pkg/tunnel/conn.go b/pkg/tunnel/conn.go index 77383f4..a4c8329 100644 --- a/pkg/tunnel/conn.go +++ b/pkg/tunnel/conn.go @@ -71,6 +71,10 @@ type Connection struct { meta *MetaShareUserMessage currentOnlineUsers map[string]MetaShareUserMessage + + invalidPerm atomic.Bool + invalidPermData []byte + invalidPermTime time.Time } func (t *Connection) SendWsMessage(msg guacd.Instruction) error { @@ -124,7 +128,7 @@ func (t *Connection) Run(ctx *gin.Context) (err error) { err = t.SendWsMessage(guacd.NewInstruction( INTERNALDATAOPCODE, t.guacdTunnel.UUID())) if err != nil { - logger.Error("Run err: ", err) + logger.Errorf("Run err: %s", err) return err } eventChan := t.Cache.GetSessionEventChan(t.Sess.ID) @@ -182,7 +186,7 @@ func (t *Connection) Run(ctx *gin.Context) (err error) { switch instruction.Opcode { case guacd.InstructionClientNop: - if time.Now().Sub(noNopTime) > maxNopTimeout { + if time.Since(noNopTime) > maxNopTimeout { logger.Errorf("Session[%s] guacamole server nop timeout", t) if requiredErr.Opcode != "" { logger.Errorf("Session[%s] send guacamole server required err: %s", t, @@ -377,6 +381,10 @@ func (t *Connection) HandleTask(task *model.TerminalTask) error { _ = t.SendWsMessage(ins.Instruction()) reason := model.SessionLifecycleLog{Reason: string(model.ReasonErrAdminTerminate)} t.Service.RecordLifecycleLog(t.Sess.ID, model.AssetConnectFinished, reason) + case model.TaskPermExpired: + t.PermBecomeExpired(task.Name, task.Args) + case model.TaskPermValid: + t.PermBecomeValid(task.Name, task.Args) default: return fmt.Errorf("unknown task %s", task.Name) } @@ -389,7 +397,14 @@ func (t *Connection) String() string { } func (t *Connection) IsPermissionExpired(now time.Time) bool { - return t.Sess.ExpireInfo.IsExpired(now) + if t.Sess.ExpireInfo.IsExpired(now) { + return true + } + if t.invalidPerm.Load() { + maxInvalidTime := t.invalidPermTime.Add(10 * time.Minute) + return now.After(maxInvalidTime) + } + return false } func (t *Connection) CloneMonitorTunnel() (*guacd.Tunnel, error) { @@ -532,3 +547,27 @@ func (t *Connection) notifySessionAction(action string, user string) { t.Cache.BroadcastSessionEvent(t.Sess.ID, &Event{Type: action, Data: p}) } + +func (t *Connection) PermBecomeExpired(code, detail string) { + if t.invalidPerm.Load() { + return + } + t.invalidPermTime = time.Now() + t.invalidPerm.Store(true) + p, _ := json.Marshal(map[string]string{"code": code, "detail": detail}) + t.invalidPermData = p + t.Cache.BroadcastSessionEvent(t.Sess.ID, + &Event{Type: PermExpiredEvent, Data: p}) +} + +func (t *Connection) PermBecomeValid(code, detail string) { + if !t.invalidPerm.Load() { + return + } + t.invalidPermTime = time.Now() + t.invalidPerm.Store(false) + p, _ := json.Marshal(map[string]string{"code": code, "detail": detail}) + t.invalidPermData = p + t.Cache.BroadcastSessionEvent(t.Sess.ID, + &Event{Type: PermValidEvent, Data: p}) +} diff --git a/pkg/tunnel/stream_output.go b/pkg/tunnel/stream_output.go index af4a359..608d963 100644 --- a/pkg/tunnel/stream_output.go +++ b/pkg/tunnel/stream_output.go @@ -78,7 +78,9 @@ func (filter *OutputStreamInterceptingFilter) handleBlob(unfilteredInstruction * } return nil } - stream.recorder.RecordWrite(stream.ftpLog, blob) + if err1 := stream.recorder.RecordWrite(stream.ftpLog, blob); err1 != nil { + logger.Errorf("OutputStream filter stream %s record write err: %+v", stream.streamIndex, err1) + } if !filter.acknowledgeBlobs { filter.acknowledgeBlobs = true ins := guacd.NewInstruction(guacd.InstructionStreamingBlob, index, "") diff --git a/ui/src/components/GuacamoleConnect.vue b/ui/src/components/GuacamoleConnect.vue index a04b737..4ce902a 100644 --- a/ui/src/components/GuacamoleConnect.vue +++ b/ui/src/components/GuacamoleConnect.vue @@ -247,7 +247,8 @@ export default { shareCode: null, shareLoading: false, enableShare: true, - onlineUsersMap: {} + onlineUsersMap: {}, + warningIntervalId: null } }, computed: { @@ -391,6 +392,9 @@ export default { this.startConnect() window.addEventListener('message', this.handleEventFromLuna, false) }, + destroyed() { + clearInterval(this.warningIntervalId) + }, methods: { generateShareURL() { return `${BASE_URL}/lion/share/${this.shareId}/` @@ -973,6 +977,19 @@ export default { this.onlineUsersMap = dataObj break } + case 'perm_expired': { + const warningMsg = `${this.t('PermissionExpired')}: ${dataObj.detail}` + this.$message.warning(warningMsg) + this.warningIntervalId = setInterval(() => { + this.$message.warning(warningMsg) + }, 1000 * 31) + break + } + case 'perm_valid': { + clearInterval(this.warningIntervalId) + this.$message.info(`${this.t('PermissionValid')}`) + break + } default: break } diff --git a/ui/src/components/GuacamoleShare.vue b/ui/src/components/GuacamoleShare.vue index cf1f6e3..5e108fa 100644 --- a/ui/src/components/GuacamoleShare.vue +++ b/ui/src/components/GuacamoleShare.vue @@ -50,7 +50,8 @@ export default { onlineUsersMap: {}, share_id: '', recordId: '', - locked: false + locked: false, + warningIntervalId: null } }, computed: { @@ -79,6 +80,9 @@ export default { ] } }, + destroyed() { + clearInterval(this.warningIntervalId) + }, methods: { submitCode() { if (this.code === '') { @@ -140,6 +144,19 @@ export default { this.locked = false break } + case 'perm_expired': { + const warningMsg = `${this.t('PermissionExpired')}: ${dataObj.detail}` + this.$message.warning(warningMsg) + this.warningIntervalId = setInterval(() => { + this.$message.warning(warningMsg) + }, 1000 * 31) + break + } + case 'perm_valid': { + clearInterval(this.warningIntervalId) + this.$message.info(`${this.t('PermissionValid')}`) + break + } } } }