From 26f85e1241717d251cb7bef75e7ad52d4b3ae2d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=92=B2=E5=85=88=E7=94=9F?= <821039958@qq.com> Date: Tue, 15 Jul 2025 17:07:50 +0800 Subject: [PATCH] =?UTF-8?q?=E7=BB=88=E7=AB=AF=E6=A8=A1=E5=9D=97=E4=B8=8A?= =?UTF-8?q?=EF=BC=88ssh=E8=BF=9E=E6=8E=A5=20=E5=91=BD=E4=BB=A4=E6=89=A7?= =?UTF-8?q?=E8=A1=8C=20=EF=BC=8CSftp=E6=96=87=E4=BB=B6=E4=B8=8A=E4=BC=A0?= =?UTF-8?q?=EF=BC=8C=E4=B8=8B=E8=BD=BD=20=E5=8F=8A=E6=96=87=E6=9C=AC?= =?UTF-8?q?=E6=93=8D=E4=BD=9C=E5=8A=9F=E8=83=BD=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dependencies/pom.xml | 3 +- .../config/OpsWebSocketAutoConfiguration.java | 13 + .../websocket/config/WebSocketProperties.java | 11 + .../handler/JsonWebSocketMessageHandler.java | 17 +- modules/module-ci-terminal/pom.xml | 52 ++++ .../common/AbstractGenericsDataStrategy.java | 74 +++++ .../terminal/common/AesEncryptUtils.java | 150 ++++++++++ .../module/terminal/common/AesEncryptor.java | 36 +++ .../module/terminal/common/AppConst.java | 31 +++ .../module/terminal/common/BooleanBit.java | 67 +++++ .../casic/module/terminal/common/CnConst.java | 21 ++ .../casic/module/terminal/common/Const.java | 35 +++ .../module/terminal/common/ErrorMessage.java | 169 ++++++++++++ .../terminal/common/ExtraFieldConst.java | 51 ++++ .../module/terminal/common/FieldConst.java | 89 ++++++ .../terminal/common/GenericsDataModel.java | 36 +++ .../terminal/common/GenericsDataStrategy.java | 53 ++++ .../common/GenericsStrategyDefinition.java | 75 +++++ .../common/InitializingOperatorTypes.java | 38 +++ .../terminal/common/OperatorLogModel.java | 103 +++++++ .../module/terminal/common/OperatorLogs.java | 193 +++++++++++++ .../terminal/common/OperatorRiskLevel.java | 33 +++ .../module/terminal/common/OperatorType.java | 53 ++++ .../common/OperatorTypeDefinition.java | 21 ++ .../terminal/common/OperatorTypeHolder.java | 40 +++ .../module/terminal/common/RsaDecryptor.java | 21 ++ .../terminal/common/RsaDecryptorImpl.java | 36 +++ .../module/terminal/common/TraceIdHolder.java | 99 +++++++ .../terminal/common/UpdatePasswordAction.java | 36 +++ .../terminal/common/WebSocketSyncSession.java | 119 ++++++++ .../module/terminal/common/WebSockets.java | 128 +++++++++ .../module/terminal/common/WsCloseCode.java | 27 ++ .../terminal/common/annotation/Module.java | 25 ++ .../terminal/common/config/ConfigRef.java | 58 ++++ .../terminal/common/config/ConfigRefImpl.java | 60 ++++ .../terminal/common/config/ConfigStore.java | 244 ++++++++++++++++ .../common/config/ManagementConfigStore.java | 33 +++ .../config/ManagementConfigStoreImpl.java | 202 ++++++++++++++ .../terminal/common/constant/ConfigKeys.java | 35 +++ .../terminal/common/holder/SpringHolder.java | 132 +++++++++ .../configuration/SpringConfiguration.java | 26 ++ .../controller/TerminalController.java | 46 ++++ .../controller/TerminalSftpController.java | 63 +++++ .../controller/dto/TerminalAccessDTO.java | 31 +++ .../controller/dto/TerminalConnectDTO.java | 86 ++++++ .../controller/dto/TerminalTransferDTO.java | 30 ++ .../TerminalConnectLogQueryRequest.java | 67 +++++ .../controller/vo/TerminalConnectLogVO.java | 63 +++++ .../convert/TerminalConnectLogConvert.java | 37 +++ .../dal/dataobject/TerminalConnectLogDO.java | 76 +++++ .../dal/mysql/TerminalConnectLogMapper.java | 18 ++ .../terminal/dal/redis/TerminalRedisDAO.java | 111 ++++++++ .../terminal/define/AssetThreadPools.java | 110 ++++++++ .../define/cache/TerminalCacheKeyDefine.java | 54 ++++ .../terminal/define/config/AppSftpConfig.java | 51 ++++ .../define/operator/TerminalOperatorType.java | 69 +++++ .../terminal/enums/HostExtraItemEnum.java | 54 ++++ .../enums/HostExtraSshAuthTypeEnum.java | 41 +++ .../terminal/enums/HostIdentityTypeEnum.java | 36 +++ .../terminal/enums/HostSshAuthTypeEnum.java | 41 +++ .../module/terminal/enums/HostStatusEnum.java | 36 +++ .../module/terminal/enums/HostTypeEnum.java | 48 ++++ .../enums/TerminalConnectTypeEnum.java | 36 +++ .../host/config/model/HostSshConfigModel.java | 100 +++++++ .../strategy/HostSshConfigStrategy.java | 128 +++++++++ .../host/extra/model/HostLabelExtraModel.java | 32 +++ .../host/extra/model/HostSpecExtraModel.java | 119 ++++++++ .../host/extra/model/HostSshExtraModel.java | 42 +++ .../strategy/HostLabelExtraStrategy.java | 43 +++ .../extra/strategy/HostSpecExtraStrategy.java | 27 ++ .../terminal/host/jsch/SessionMessage.java | 18 ++ .../terminal/host/jsch/SessionStores.java | 134 +++++++++ .../terminal/constant/TerminalMessage.java | 26 ++ .../terminal/dto/SftpGetContentCacheDTO.java | 33 +++ .../terminal/dto/SftpSetContentCacheDTO.java | 33 +++ .../host/terminal/enums/InputTypeEnum.java | 260 ++++++++++++++++++ .../host/terminal/enums/OutputTypeEnum.java | 121 ++++++++ .../enums/TerminalConnectStatusEnum.java | 51 ++++ .../handler/AbstractTerminalHandler.java | 98 +++++++ .../terminal/handler/ITerminalHandler.java | 23 ++ .../handler/SftpChangeModeHandler.java | 65 +++++ .../SftpDownloadFlatDirectoryHandler.java | 58 ++++ .../handler/SftpGetContentHandler.java | 66 +++++ .../terminal/handler/SftpListHandler.java | 62 +++++ .../handler/SftpMakeDirectoryHandler.java | 60 ++++ .../terminal/handler/SftpMoveHandler.java | 62 +++++ .../terminal/handler/SftpRemoveHandler.java | 62 +++++ .../handler/SftpSetContentHandler.java | 78 ++++++ .../terminal/handler/SftpTouchHandler.java | 60 ++++ .../terminal/handler/SftpTruncateHandler.java | 61 ++++ .../terminal/handler/SshInputHandler.java | 28 ++ .../terminal/handler/SshResizeHandler.java | 28 ++ .../handler/TerminalCheckHandler.java | 212 ++++++++++++++ .../handler/TerminalCloseHandler.java | 27 ++ .../handler/TerminalConnectHandler.java | 169 ++++++++++++ .../terminal/handler/TerminalPingHandler.java | 36 +++ .../terminal/manager/TerminalManager.java | 94 +++++++ .../terminal/model/TerminalBasePayload.java | 31 +++ .../host/terminal/model/TerminalConfig.java | 71 +++++ .../model/request/SftpBaseRequest.java | 29 ++ .../model/request/SftpChangeModeRequest.java | 30 ++ .../SftpDownloadFlatDirectoryRequest.java | 30 ++ .../model/request/SftpListRequest.java | 30 ++ .../model/request/SftpMoveRequest.java | 30 ++ .../model/request/SshInputRequest.java | 31 +++ .../model/request/SshResizeRequest.java | 37 +++ .../model/request/TerminalCheckRequest.java | 37 +++ .../model/request/TerminalConnectRequest.java | 42 +++ .../request/TerminalConnectResponse.java | 35 +++ .../request/host/HostTestConnectRequest.java | 33 +++ .../host/TerminalConnectLogCreateRequest.java | 50 ++++ .../model/response/SftpBaseResponse.java | 35 +++ .../SftpDownloadFlatDirectoryResponse.java | 33 +++ .../terminal/model/response/SftpFileVO.java | 77 ++++++ .../response/SftpGetContentResponse.java | 29 ++ .../model/response/SftpListResponse.java | 33 +++ .../response/SftpSetContentResponse.java | 29 ++ .../model/response/SshOutputResponse.java | 30 ++ .../model/response/TerminalCheckResponse.java | 35 +++ .../model/response/TerminalCloseResponse.java | 35 +++ .../host/terminal/session/ISftpSession.java | 111 ++++++++ .../host/terminal/session/ISshSession.java | 43 +++ .../terminal/session/ITerminalSession.java | 53 ++++ .../host/terminal/session/SftpSession.java | 216 +++++++++++++++ .../host/terminal/session/SshSession.java | 123 +++++++++ .../terminal/session/TerminalSession.java | 114 ++++++++ .../host/terminal/utils/TerminalUtils.java | 56 ++++ .../host/transfer/ITransferHandler.java | 37 +++ .../host/transfer/ITransferSession.java | 83 ++++++ .../host/transfer/SftpFileBackupParams.java | 41 +++ .../host/transfer/TerminalConnection.java | 34 +++ .../transfer/TerminalTransferManager.java | 48 ++++ .../host/transfer/TransferHandler.java | 137 +++++++++ .../host/transfer/TransferOperator.java | 53 ++++ .../transfer/TransferOperatorRequest.java | 46 ++++ .../transfer/TransferOperatorResponse.java | 61 ++++ .../host/transfer/TransferReceiver.java | 63 +++++ .../host/transfer/TransferSession.java | 135 +++++++++ .../terminal/host/transfer/TransferType.java | 43 +++ .../terminal/host/transfer/TransferUtils.java | 77 ++++++ .../transfer/session/DownloadSession.java | 168 +++++++++++ .../host/transfer/session/UploadSession.java | 81 ++++++ .../terminal/service/HostConnectService.java | 34 +++ .../service/TerminalConnectLogService.java | 51 ++++ .../terminal/service/TerminalSftpService.java | 48 ++++ .../service/impl/HostConnectServiceImpl.java | 158 +++++++++++ .../impl/TerminalConnectLogServiceImpl.java | 121 ++++++++ .../service/impl/TerminalService.java | 57 ++++ .../service/impl/TerminalSftpServiceImpl.java | 167 +++++++++++ .../terminal/utils/RsaParamDecryptUtils.java | 38 +++ .../module/terminal/utils/SftpUtils.java | 50 ++++ .../cd/casic/module/terminal/utils/Valid.java | 140 ++++++++++ .../SFTPWebSocketMessageListener.java | 45 +++ .../TerminalWebSocketMessageListener.java | 70 +++++ .../TransferWebSocketMessageListener.java | 69 +++++ modules/pom.xml | 1 + ops-server/pom.xml | 5 + 157 files changed, 10189 insertions(+), 5 deletions(-) create mode 100644 modules/module-ci-terminal/pom.xml create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/AbstractGenericsDataStrategy.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/AesEncryptUtils.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/AesEncryptor.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/AppConst.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/BooleanBit.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/CnConst.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/Const.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/ErrorMessage.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/ExtraFieldConst.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/FieldConst.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/GenericsDataModel.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/GenericsDataStrategy.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/GenericsStrategyDefinition.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/InitializingOperatorTypes.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/OperatorLogModel.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/OperatorLogs.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/OperatorRiskLevel.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/OperatorType.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/OperatorTypeDefinition.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/OperatorTypeHolder.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/RsaDecryptor.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/RsaDecryptorImpl.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/TraceIdHolder.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/UpdatePasswordAction.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/WebSocketSyncSession.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/WebSockets.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/WsCloseCode.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/annotation/Module.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/config/ConfigRef.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/config/ConfigRefImpl.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/config/ConfigStore.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/config/ManagementConfigStore.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/config/ManagementConfigStoreImpl.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/constant/ConfigKeys.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/holder/SpringHolder.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/configuration/SpringConfiguration.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/controller/TerminalController.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/controller/TerminalSftpController.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/controller/dto/TerminalAccessDTO.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/controller/dto/TerminalConnectDTO.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/controller/dto/TerminalTransferDTO.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/controller/request/TerminalConnectLogQueryRequest.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/controller/vo/TerminalConnectLogVO.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/convert/TerminalConnectLogConvert.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/dal/dataobject/TerminalConnectLogDO.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/dal/mysql/TerminalConnectLogMapper.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/dal/redis/TerminalRedisDAO.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/define/AssetThreadPools.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/define/cache/TerminalCacheKeyDefine.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/define/config/AppSftpConfig.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/define/operator/TerminalOperatorType.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/enums/HostExtraItemEnum.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/enums/HostExtraSshAuthTypeEnum.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/enums/HostIdentityTypeEnum.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/enums/HostSshAuthTypeEnum.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/enums/HostStatusEnum.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/enums/HostTypeEnum.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/enums/TerminalConnectTypeEnum.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/config/model/HostSshConfigModel.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/config/strategy/HostSshConfigStrategy.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/extra/model/HostLabelExtraModel.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/extra/model/HostSpecExtraModel.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/extra/model/HostSshExtraModel.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/extra/strategy/HostLabelExtraStrategy.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/extra/strategy/HostSpecExtraStrategy.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/jsch/SessionMessage.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/jsch/SessionStores.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/constant/TerminalMessage.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/dto/SftpGetContentCacheDTO.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/dto/SftpSetContentCacheDTO.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/enums/InputTypeEnum.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/enums/OutputTypeEnum.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/enums/TerminalConnectStatusEnum.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/handler/AbstractTerminalHandler.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/handler/ITerminalHandler.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/handler/SftpChangeModeHandler.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/handler/SftpDownloadFlatDirectoryHandler.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/handler/SftpGetContentHandler.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/handler/SftpListHandler.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/handler/SftpMakeDirectoryHandler.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/handler/SftpMoveHandler.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/handler/SftpRemoveHandler.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/handler/SftpSetContentHandler.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/handler/SftpTouchHandler.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/handler/SftpTruncateHandler.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/handler/SshInputHandler.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/handler/SshResizeHandler.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/handler/TerminalCheckHandler.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/handler/TerminalCloseHandler.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/handler/TerminalConnectHandler.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/handler/TerminalPingHandler.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/manager/TerminalManager.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/model/TerminalBasePayload.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/model/TerminalConfig.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/model/request/SftpBaseRequest.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/model/request/SftpChangeModeRequest.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/model/request/SftpDownloadFlatDirectoryRequest.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/model/request/SftpListRequest.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/model/request/SftpMoveRequest.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/model/request/SshInputRequest.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/model/request/SshResizeRequest.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/model/request/TerminalCheckRequest.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/model/request/TerminalConnectRequest.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/model/request/TerminalConnectResponse.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/model/request/host/HostTestConnectRequest.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/model/request/host/TerminalConnectLogCreateRequest.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/model/response/SftpBaseResponse.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/model/response/SftpDownloadFlatDirectoryResponse.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/model/response/SftpFileVO.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/model/response/SftpGetContentResponse.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/model/response/SftpListResponse.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/model/response/SftpSetContentResponse.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/model/response/SshOutputResponse.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/model/response/TerminalCheckResponse.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/model/response/TerminalCloseResponse.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/session/ISftpSession.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/session/ISshSession.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/session/ITerminalSession.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/session/SftpSession.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/session/SshSession.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/session/TerminalSession.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/utils/TerminalUtils.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/transfer/ITransferHandler.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/transfer/ITransferSession.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/transfer/SftpFileBackupParams.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/transfer/TerminalConnection.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/transfer/TerminalTransferManager.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/transfer/TransferHandler.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/transfer/TransferOperator.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/transfer/TransferOperatorRequest.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/transfer/TransferOperatorResponse.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/transfer/TransferReceiver.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/transfer/TransferSession.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/transfer/TransferType.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/transfer/TransferUtils.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/transfer/session/DownloadSession.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/transfer/session/UploadSession.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/service/HostConnectService.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/service/TerminalConnectLogService.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/service/TerminalSftpService.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/service/impl/HostConnectServiceImpl.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/service/impl/TerminalConnectLogServiceImpl.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/service/impl/TerminalService.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/service/impl/TerminalSftpServiceImpl.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/utils/RsaParamDecryptUtils.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/utils/SftpUtils.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/utils/Valid.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/websocket/SFTPWebSocketMessageListener.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/websocket/TerminalWebSocketMessageListener.java create mode 100644 modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/websocket/TransferWebSocketMessageListener.java diff --git a/dependencies/pom.xml b/dependencies/pom.xml index 36df6fcb..572b0bc7 100644 --- a/dependencies/pom.xml +++ b/dependencies/pom.xml @@ -62,7 +62,8 @@ 33.3.1-jre 2.14.5 3.11.1 - 0.1.55 + + 0.2.15 2.9.2 2.7.0 3.0.6 diff --git a/framework/spring-boot-starter-websocket/src/main/java/cd/casic/framework/websocket/config/OpsWebSocketAutoConfiguration.java b/framework/spring-boot-starter-websocket/src/main/java/cd/casic/framework/websocket/config/OpsWebSocketAutoConfiguration.java index f8b42738..906b6608 100644 --- a/framework/spring-boot-starter-websocket/src/main/java/cd/casic/framework/websocket/config/OpsWebSocketAutoConfiguration.java +++ b/framework/spring-boot-starter-websocket/src/main/java/cd/casic/framework/websocket/config/OpsWebSocketAutoConfiguration.java @@ -26,6 +26,7 @@ import org.springframework.web.socket.WebSocketHandler; import org.springframework.web.socket.config.annotation.EnableWebSocket; import org.springframework.web.socket.config.annotation.WebSocketConfigurer; import org.springframework.web.socket.server.HandshakeInterceptor; +import org.springframework.web.socket.server.standard.ServletServerContainerFactoryBean; import java.util.List; @@ -76,6 +77,18 @@ public class OpsWebSocketAutoConfiguration { return new WebSocketAuthorizeRequestsCustomizer(webSocketProperties); } + /** + * @return websocket 缓冲区大小配置 + */ + @Bean + public ServletServerContainerFactoryBean servletServerContainerFactoryBean(WebSocketProperties config) { + ServletServerContainerFactoryBean factory = new ServletServerContainerFactoryBean(); + factory.setMaxBinaryMessageBufferSize(config.getBinaryBufferSize()); + factory.setMaxTextMessageBufferSize(config.getBinaryBufferSize()); + factory.setMaxSessionIdleTimeout(config.getSessionIdleTimeout()); + return factory; + } + // ==================== Sender 相关 ==================== @Configuration diff --git a/framework/spring-boot-starter-websocket/src/main/java/cd/casic/framework/websocket/config/WebSocketProperties.java b/framework/spring-boot-starter-websocket/src/main/java/cd/casic/framework/websocket/config/WebSocketProperties.java index 554b08bd..4553f661 100644 --- a/framework/spring-boot-starter-websocket/src/main/java/cd/casic/framework/websocket/config/WebSocketProperties.java +++ b/framework/spring-boot-starter-websocket/src/main/java/cd/casic/framework/websocket/config/WebSocketProperties.java @@ -30,4 +30,15 @@ public class WebSocketProperties { @NotNull(message = "WebSocket 的消息发送者不能为空") private String senderType = "local"; + /** + * 二进制消息缓冲区大小 byte + */ + private Integer binaryBufferSize; + + /** + * session 最大超时时间 ms + */ + private Long sessionIdleTimeout; + + } diff --git a/framework/spring-boot-starter-websocket/src/main/java/cd/casic/framework/websocket/core/handler/JsonWebSocketMessageHandler.java b/framework/spring-boot-starter-websocket/src/main/java/cd/casic/framework/websocket/core/handler/JsonWebSocketMessageHandler.java index f1c3becf..63400035 100644 --- a/framework/spring-boot-starter-websocket/src/main/java/cd/casic/framework/websocket/core/handler/JsonWebSocketMessageHandler.java +++ b/framework/spring-boot-starter-websocket/src/main/java/cd/casic/framework/websocket/core/handler/JsonWebSocketMessageHandler.java @@ -8,9 +8,7 @@ import cd.casic.framework.websocket.core.util.WebSocketFrameworkUtils; import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.TypeUtil; import lombok.extern.slf4j.Slf4j; -import org.springframework.web.socket.TextMessage; -import org.springframework.web.socket.WebSocketHandler; -import org.springframework.web.socket.WebSocketSession; +import org.springframework.web.socket.*; import org.springframework.web.socket.handler.TextWebSocketHandler; import java.lang.reflect.Type; @@ -79,5 +77,16 @@ public class JsonWebSocketMessageHandler extends TextWebSocketHandler { log.error("[handleTextMessage][session({}) message({}) 处理异常]", session.getId(), message.getPayload()); } } - + //处理二进制消息 + @Override + protected void handleBinaryMessage(WebSocketSession session, BinaryMessage message) { + try { + WebSocketMessageListener messageListener = listeners.get("SFTP-access-message-send"); + Long tenantId = WebSocketFrameworkUtils.getTenantId(session); + TenantUtils.execute(tenantId, () -> messageListener.onMessage(session, message)); + } + catch (Throwable ex) { + log.error("[handleBinaryMessage][session({}) message({}) 处理异常]", session.getId(), message.getPayload()); + } + } } diff --git a/modules/module-ci-terminal/pom.xml b/modules/module-ci-terminal/pom.xml new file mode 100644 index 00000000..e4cc9b8a --- /dev/null +++ b/modules/module-ci-terminal/pom.xml @@ -0,0 +1,52 @@ + + + 4.0.0 + + cd.casic.boot + modules + ${revision} + + + + + jar + ${project.artifactId} + ${revision} + module-ci-terminal + + + + cd.casic.boot + module-ci-machine + ${revision} + + + cd.casic.boot + module-ci-commons + + + cd.casic.boot + spring-boot-starter-web + + + cd.casic.boot + spring-boot-starter-websocket + + + cd.casic.boot + spring-boot-starter-redis + + + + + + cn.orionsec.kit + orion-net + 2.0.1 + + + + + \ No newline at end of file diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/AbstractGenericsDataStrategy.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/AbstractGenericsDataStrategy.java new file mode 100644 index 00000000..4e410b35 --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/AbstractGenericsDataStrategy.java @@ -0,0 +1,74 @@ + +package cd.casic.module.terminal.common; + +import com.alibaba.fastjson.JSON; + +/** + * 标准数据处理策略 基类 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2024/6/11 21:44 + */ +public abstract class AbstractGenericsDataStrategy implements GenericsDataStrategy { + + protected final Class modelClass; + + public AbstractGenericsDataStrategy(Class modelClass) { + this.modelClass = modelClass; + } + + /** + * 更新填充 + * + * @param beforeModel 修改前的配置 + * @param afterModel 修改后的配置 + */ + protected void updateFill(M beforeModel, M afterModel) { + } + + /** + * 预校验参数 + * + * @param model model + */ + protected void preValid(M model) { + } + + /** + * 校验参数 + * + * @param model model + */ + protected void valid(M model) { + } + + @Override + public void doValid(M beforeModel, M afterModel) { + // 预校验参数 + this.preValid(afterModel); + // 更新填充 + this.updateFill(beforeModel, afterModel); + // 校验参数 + this.valid(afterModel); + } + + @Override + public M parse(String serialModel) { + return JSON.parseObject(serialModel, modelClass); + } + + @Override + public void toView(M model) { + } + + @Override + public M toView(String serialModel) { + // 解析 + M parse = this.parse(serialModel); + // 转为视图对象 + this.toView(parse); + return parse; + } + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/AesEncryptUtils.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/AesEncryptUtils.java new file mode 100644 index 00000000..e90b7faa --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/AesEncryptUtils.java @@ -0,0 +1,150 @@ + +package cd.casic.module.terminal.common; + +import cn.orionsec.kit.lang.utils.Exceptions; + +/** + * aes 数据加密工具类 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2023/7/8 0:05 + */ +public class AesEncryptUtils { + + private static AesEncryptor delegate; + + private AesEncryptUtils() { + } + + /** + * 加密 + * + * @param plain 明文 + * @return 密文 + */ + public static byte[] encrypt(byte[] plain) { + return delegate.encrypt(plain); + } + + /** + * 加密 + * + * @param plain 明文 + * @return 密文 + */ + public static byte[] encrypt(String plain) { + return delegate.encrypt(plain); + } + + /** + * 加密 + * + * @param plain 明文 + * @return 密文 + */ + public static String encryptAsString(String plain) { + return delegate.encryptAsString(plain); + } + + /** + * 加密 + * + * @param plain 明文 + * @return 密文 + */ + public static String encryptAsString(byte[] plain) { + return delegate.encryptAsString(plain); + } + + /** + * 解密 + * + * @param text 密文 + * @return 明文 + */ + public static byte[] decrypt(byte[] text) { + return delegate.decrypt(text); + } + + /** + * 解密 + * + * @param text 密文 + * @return 明文 + */ + public static byte[] decrypt(String text) { + return delegate.decrypt(text); + } + + /** + * 解密 + * + * @param text 密文 + * @return 明文 + */ + public static String decryptAsString(String text) { + return delegate.decryptAsString(text); + } + + /** + * 解密 + * + * @param text 密文 + * @return 明文 + */ + public static String decryptAsString(byte[] text) { + return delegate.decryptAsString(text); + } + + /** + * 验证加密结果 + * + * @param plain 明文 + * @param text 密文 + * @return 是否成功 + */ + public static boolean verify(String plain, String text) { + return delegate.verify(plain, text); + } + + /** + * 验证加密结果 + * + * @param plain 明文 + * @param text 密文 + * @return 是否成功 + */ + public static boolean verify(byte[] plain, byte[] text) { + return delegate.verify(plain, text); + } + + /** + * 加密后 base62 编码 + * + * @param plain 明文 + * @return 密文 + */ + public static String encryptBase62(String plain) { + return delegate.encryptBase62(plain); + } + + /** + * base62 解码后解密 + * + * @param text 密文 + * @return 明文 + */ + public static String decryptBase62(String text) { + return delegate.decryptBase62(text); + } + + public static void setDelegate(AesEncryptor delegate) { + if (AesEncryptUtils.delegate != null) { + // unmodified + throw Exceptions.state(); + } + AesEncryptUtils.delegate = delegate; + } + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/AesEncryptor.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/AesEncryptor.java new file mode 100644 index 00000000..3dead6ed --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/AesEncryptor.java @@ -0,0 +1,36 @@ + +package cd.casic.module.terminal.common; + +import cn.orionsec.kit.lang.utils.codec.Base62s; +import cn.orionsec.kit.lang.utils.crypto.symmetric.SymmetricCrypto; + +/** + * aes 加密器 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2023/7/8 0:20 + */ +public interface AesEncryptor extends SymmetricCrypto { + + /** + * 加密后 base62 编码 + * + * @param plain 明文 + * @return 密文 + */ + default String encryptBase62(String plain) { + return new String(Base62s.encode(this.encrypt(plain))); + } + + /** + * base62 解码后解密 + * + * @param text 密文 + * @return 明文 + */ + default String decryptBase62(String text) { + return new String(this.decrypt(Base62s.decode(text))); + } + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/AppConst.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/AppConst.java new file mode 100644 index 00000000..464d2fdb --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/AppConst.java @@ -0,0 +1,31 @@ + +package cd.casic.module.terminal.common; + +import cn.orionsec.kit.lang.constant.OrionConst; + +/** + * 项目常量 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2023/6/19 18:56 + */ +public interface AppConst extends OrionConst { + + /** + * 同 ${orion.version} 迭代时候需要手动更改 + */ + String VERSION = "2.3.9"; + + /** + * 同 ${spring.application.name} + */ + String APP_NAME = "orion-visor"; + + String GITHUB = "https://github.com/dromara/orion-visor"; + + String GITEE = "https://gitee.com/dromara/orion-visor"; + + String ISSUES = "https://github.com/dromara/orion-visor/issues"; + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/BooleanBit.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/BooleanBit.java new file mode 100644 index 00000000..e51b07eb --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/BooleanBit.java @@ -0,0 +1,67 @@ + +package cd.casic.module.terminal.common; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * boolean 枚举 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2023/9/19 10:32 + */ +@Getter +@AllArgsConstructor +public enum BooleanBit { + + /** + * 假 + */ + FALSE(0), + + /** + * 真 + */ + TRUE(1), + + ; + + private final Integer value; + + /** + * 是否为布尔值 + * + * @return boolean + */ + public boolean booleanValue() { + return this == TRUE; + } + + public static BooleanBit of(boolean value) { + return value ? TRUE : FALSE; + } + + public static BooleanBit of(Integer value) { + if (value == null) { + return null; + } + for (BooleanBit e : values()) { + if (e.value.equals(value)) { + return e; + } + } + return null; + } + + /** + * 转为布尔值 + * + * @param value value + * @return boolean + */ + public static boolean toBoolean(Integer value) { + return TRUE.value.equals(value); + } + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/CnConst.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/CnConst.java new file mode 100644 index 00000000..c9975baf --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/CnConst.java @@ -0,0 +1,21 @@ + +package cd.casic.module.terminal.common; + +/** + * 常量 - 中文 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2023/11/30 21:00 + */ +public interface CnConst { + + String CN_USER = "用户"; + + String CN_ROLE = "角色"; + + String CN_UNKNOWN = "未知"; + + String CN_INTRANET_IP = "内网IP"; + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/Const.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/Const.java new file mode 100644 index 00000000..ad338315 --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/Const.java @@ -0,0 +1,35 @@ + +package cd.casic.module.terminal.common; + +/** + * 常量 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2023/6/23 18:49 + */ +public interface Const extends cn.orionsec.kit.lang.constant.Const,FieldConst, CnConst{ + + Integer NOT_DELETE = 0; + + Integer IS_DELETED = 1; + + int BEARER_PREFIX_LEN = 7; + + int MD5_LEN = 32; + + Long ROOT_PARENT_ID = 0L; + + Integer DEFAULT_SORT = 10; + + Long NONE_ID = -1L; + + Integer DEFAULT_VERSION = 1; + + Long SYSTEM_USER_ID = 0L; + + String SYSTEM_USERNAME = "system"; + + int BATCH_COUNT = 500; + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/ErrorMessage.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/ErrorMessage.java new file mode 100644 index 00000000..d4f18743 --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/ErrorMessage.java @@ -0,0 +1,169 @@ + +package cd.casic.module.terminal.common; + +import cn.orionsec.kit.lang.exception.ApplicationException; +import cn.orionsec.kit.lang.exception.argument.InvalidArgumentException; + +/** + * 错误信息 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2023/7/7 18:51 + */ +public interface ErrorMessage { + + String MISSING = "{} 不能为空"; + + String PARAM_MISSING = "参数不能为空"; + + String PARAM_ERROR = "参数错误"; + + String ID_MISSING = "id 不能为空"; + + String INVALID_PARAM = "参数验证失败"; + + String DATA_MODIFIED = "数据发生变更, 请刷新后重试"; + + String DATA_ABSENT = "数据不存在"; + + String KEY_ABSENT = "主机密钥不存在"; + + String IDENTITY_ABSENT = "主机身份不存在"; + + String CONFIG_ABSENT = "配置不存在"; + + String CONFIG_PRESENT = "配置已存在"; + + String DATA_PRESENT = "数据已存在"; + + String NAME_PRESENT = "名称已存在"; + + String CODE_PRESENT = "编码已存在"; + + String NICKNAME_PRESENT = "花名已存在"; + + String USERNAME_PRESENT = "用户名已存在"; + + String ROLE_ABSENT = "角色不存在"; + + String ROLE_CODE_ABSENT = "角色 [{}] 不存在"; + + String INVALID_PARENT_MENU = "所选父菜单不合法"; + + String PARENT_MENU_ABSENT = "父菜单不存在"; + + String USERNAME_PASSWORD_ERROR = "用户名或密码错误"; + + String MAX_LOGIN_FAILED = "登录失败次数已上限"; + + String HISTORY_ABSENT = "历史值不存在"; + + String USER_ABSENT = "用户不存在"; + + String HOST_ABSENT = "主机不存在"; + + String GROUP_ABSENT = "分组不存在"; + + String HOST_TYPE_ERROR = "主机类型错误"; + + String HOST_NOT_ENABLED = "主机未启用"; + + String CONFIG_NOT_ENABLED = "配置未启用"; + + String UNABLE_OPERATE_ADMIN_ROLE = "无法操作管理员账号"; + + String UNSUPPORTED_CHARSET = "不支持的编码 [{}]"; + + String DECRYPT_ERROR = "数据解密失败"; + + String PASSWORD_MISSING = "请输入密码"; + + String BEFORE_PASSWORD_ERROR = "原密码错误"; + + String DATA_NO_PERMISSION = "数据无权限"; + + String ANY_NO_PERMISSION = "{}无权限"; + + String SESSION_PRESENT = "会话已存在"; + + String SESSION_ABSENT = "会话不存在"; + + String PATH_NOT_NORMALIZE = "路径不合法"; + + String OPERATE_ERROR = "操作失败"; + + String UNKNOWN_TYPE = "未知类型"; + + String ERROR_TYPE = "错误的类型"; + + String FILE_ABSENT = "文件不存在"; + + String FILE_ABSENT_CLEAR = "文件不存在 (可能已被清理)"; + + String LOG_ABSENT = "日志不存在"; + + String TASK_ABSENT = "任务不存在"; + + String CONNECT_ERROR = "连接失败"; + + String AUTH_ERROR = "认证失败"; + + String FILE_UPLOAD_ERROR = "文件上传失败"; + + String SCRIPT_UPLOAD_ERROR = "脚本上传失败"; + + String EXEC_ERROR = "执行失败"; + + String ILLEGAL_STATUS = "当前状态不支持此操作"; + + String CHECK_AUTHORIZED_HOST = "请选择已授权的主机"; + + String FILE_READ_ERROR = "文件读取失败"; + + String FILE_READ_ERROR_CLEAR = "文件读取失败 (可能已被清理)"; + + String PLEASE_CHECK_HOST_SSH = "请检查主机 {} 是否存在/权限/SSH配置"; + + String CLIENT_ABORT = "手动中断"; + + String UNABLE_DOWNLOAD_FOLDER = "无法下载文件夹"; + + /** + * 是否为业务异常 + * + * @param ex ex + * @return biz exception + */ + static boolean isBizException(Exception ex) { + if (ex == null) { + return false; + } + return ex instanceof InvalidArgumentException + || ex instanceof IllegalArgumentException + || ex instanceof ApplicationException; + } + + /** + * 获取错误信息 + * + * @param ex ex + * @param defaultMsg defaultMsg + * @return message + */ + static String getErrorMessage(Exception ex, String defaultMsg) { + if (ex == null) { + return null; + } + String message = ex.getMessage(); + if (message == null) { + return defaultMsg; + } + // 业务异常 + if (isBizException(ex)) { + return message; + } + return defaultMsg; + } + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/ExtraFieldConst.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/ExtraFieldConst.java new file mode 100644 index 00000000..1dcb8596 --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/ExtraFieldConst.java @@ -0,0 +1,51 @@ + +package cd.casic.module.terminal.common; + +/** + * 额外字段常量 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2023/12/28 18:34 + */ +public interface ExtraFieldConst extends FieldConst { + + String USER_ID = "userId"; + + String TRACE_ID = "traceId"; + + String IDENTITY = "identity"; + + String GROUP_NAME = "groupName"; + + String ID_LIST = "idList"; + + String USERNAME = "username"; + + String HOME = "home"; + + String STATUS_NAME = "statusName"; + + String KEY_NAME = "keyName"; + + String POSITION_NAME = "positionName"; + + String GRANT_TYPE = "grantType"; + + String GRANT_NAME = "grantName"; + + String CHANNEL_ID = "channelId"; + + String SESSION_ID = "sessionId"; + + String CONNECT_TYPE = "connectType"; + + String HOST_ID = "hostId"; + + String HOST_NAME = "hostName"; + + String LOG_ID = "logId"; + + String DARK = "dark"; + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/FieldConst.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/FieldConst.java new file mode 100644 index 00000000..5d241d87 --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/FieldConst.java @@ -0,0 +1,89 @@ + +package cd.casic.module.terminal.common; + +/** + * 字段常量 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2023/10/17 12:44 + */ +public interface FieldConst { + + String ID = "id"; + + String KEY = "key"; + + String CODE = "code"; + + String NAME = "name"; + + String TITLE = "title"; + + String VALUE = "value"; + + String LABEL = "label"; + + String TYPE = "type"; + + String COLOR = "color"; + + String STATUS = "status"; + + String INFO = "info"; + + String EXTRA = "extra"; + + String REL_ID = "relId"; + + String BEFORE = "before"; + + String AFTER = "after"; + + String SOURCE = "source"; + + String TARGET = "target"; + + String CHARSET = "charset"; + + String TOKEN = "token"; + + String SEQ = "seq"; + + String PATH = "path"; + + String ADDRESS = "address"; + + String MOD = "mod"; + + String COUNT = "count"; + + String DATE = "date"; + + String TIME = "time"; + + String TEXT = "text"; + + String ISSUE = "issue"; + + String EXPIRE = "expire"; + + String LOCATION = "location"; + + String USER_AGENT = "userAgent"; + + String ERROR_MESSAGE = "errorMessage"; + + String UUID = "uuid"; + + String REDIRECT = "redirect"; + + String SCHEMA = "schema"; + + String FILTER = "filter"; + + String ALL = "all"; + + String CONFIG = "config"; + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/GenericsDataModel.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/GenericsDataModel.java new file mode 100644 index 00000000..c00bb6f5 --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/GenericsDataModel.java @@ -0,0 +1,36 @@ + +package cd.casic.module.terminal.common; + +import com.alibaba.fastjson.JSON; + +import java.util.Map; + +/** + * 标准数据模型 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2023/12/20 22:07 + */ +public interface GenericsDataModel { + + /** + * 序列化 + * + * @return json + */ + default String serial() { + return JSON.toJSONString(this); + } + + + /** + * 转为 map + * + * @return map + */ + default Map toMap() { + return JSON.parseObject(this.serial()); + } + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/GenericsDataStrategy.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/GenericsDataStrategy.java new file mode 100644 index 00000000..d2d1026d --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/GenericsDataStrategy.java @@ -0,0 +1,53 @@ + +package cd.casic.module.terminal.common; + +/** + * 标准数据处理策略 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2023/12/20 22:09 + */ +public interface GenericsDataStrategy { + + /** + * 获取默认值 + * + * @return 默认值 + */ + M getDefault(); + + /** + * 执行完整验证链 + *

+ * preValid > updateFill > valid + * + * @param beforeModel beforeModel + * @param afterModel afterModel + */ + void doValid(M beforeModel, M afterModel); + + /** + * 解析数据 + * + * @param serialModel serialModel + * @return model + */ + M parse(String serialModel); + + /** + * 转为视图配置 + * + * @param model model + */ + void toView(M model); + + /** + * 转为视图配置 + * + * @param serialModel serialModel + * @return model + */ + M toView(String serialModel); + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/GenericsStrategyDefinition.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/GenericsStrategyDefinition.java new file mode 100644 index 00000000..1e705776 --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/GenericsStrategyDefinition.java @@ -0,0 +1,75 @@ +package cd.casic.module.terminal.common; + +import cd.casic.module.terminal.common.holder.SpringHolder; + +/** + * 标准数据定义 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2023/12/21 0:07 + */ +@SuppressWarnings("unchecked") +public interface GenericsStrategyDefinition { + + /** + * 获取数据处理策略 + * + * @return class + */ + Class> getStrategyClass(); + + /** + * 获取数据模型策略处理器 + * + * @param Model + * @param Strategy + * @return Strategy Bean + */ + default > S getStrategy() { + return (S) SpringHolder.getBean(this.getStrategyClass()); + } + + /** + * 获取默认值 + * + * @param model + * @return model + */ + default M getDefault() { + return (M) this.getStrategy().getDefault(); + } + + /** + * 执行完整验证链 + * + * @param beforeModel beforeModel + * @param afterModel afterModel + */ + default void doValid(GenericsDataModel beforeModel, GenericsDataModel afterModel) { + this.getStrategy().doValid(beforeModel, afterModel); + } + + /** + * 反序列化对象 + * + * @param serialModel serialModel + * @param model + * @return model + */ + default M parse(String serialModel) { + return (M) this.getStrategy().parse(serialModel); + } + + /** + * 转为视图对象 + * + * @param serialModel serialModel + * @param model + * @return viewModel + */ + default M toView(String serialModel) { + return (M) this.getStrategy().toView(serialModel); + } + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/InitializingOperatorTypes.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/InitializingOperatorTypes.java new file mode 100644 index 00000000..280e11cb --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/InitializingOperatorTypes.java @@ -0,0 +1,38 @@ + +package cd.casic.module.terminal.common; + +import cn.orionsec.kit.lang.utils.Arrays1; +import jakarta.annotation.PostConstruct; +import cd.casic.module.terminal.common.annotation.Module; + + +/** + * 操作类型初始化器 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2023/10/13 17:45 + */ +public abstract class InitializingOperatorTypes implements OperatorTypeDefinition { + + @PostConstruct + public void init() { + // 获取模块注解 + Module moduleDefinition = this.getClass().getAnnotation(Module.class); + if (moduleDefinition == null) { + return; + } + // 获取类型 + OperatorType[] types = this.types(); + if (Arrays1.isEmpty(types)) { + return; + } + // 定义类型 + String module = moduleDefinition.value(); + for (OperatorType type : types) { + type.setModule(module); + OperatorTypeHolder.set(type); + } + } + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/OperatorLogModel.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/OperatorLogModel.java new file mode 100644 index 00000000..9738dd2c --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/OperatorLogModel.java @@ -0,0 +1,103 @@ + +package cd.casic.module.terminal.common; + +import lombok.Data; + +import java.util.Date; + +/** + * 操作日志模型 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2023/10/9 18:44 + */ +@Data +public class OperatorLogModel{ + + /** + * userId + */ + private Long userId; + + /** + * 用户名 + */ + private String username; + + /** + * traceId + */ + private String traceId; + + /** + * 请求 ip + */ + private String address; + + /** + * 请求地址 + */ + private String location; + + /** + * user-agent + */ + private String userAgent; + + /** + * 日志 + */ + private String logInfo; + + /** + * 风险等级 + */ + private String riskLevel; + + /** + * 模块 + */ + private String module; + + /** + * 操作类型 + */ + private String type; + + /** + * 参数 + */ + private String extra; + + /** + * 操作结果 0失败 1成功 + */ + private Integer result; + + /** + * 错误信息 + */ + private String errorMessage; + + /** + * 返回值 + */ + private String returnValue; + + /** + * 操作时间 + */ + private Integer duration; + + /** + * 开始时间 + */ + private Date startTime; + + /** + * 结束时间 + */ + private Date endTime; + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/OperatorLogs.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/OperatorLogs.java new file mode 100644 index 00000000..c39b6025 --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/OperatorLogs.java @@ -0,0 +1,193 @@ + +package cd.casic.module.terminal.common; + +import cd.casic.framework.security.core.LoginUser; +import cn.orionsec.kit.lang.utils.Exceptions; +import cn.orionsec.kit.lang.utils.Strings; +import cn.orionsec.kit.lang.utils.collect.Maps; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import com.alibaba.fastjson.serializer.SerializeFilter; +import java.util.Map; + +/** + * 操作日志工具类 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2023/10/10 11:32 + */ +public class OperatorLogs implements ExtraFieldConst { + + private static final String UN_SAVE_FLAG = "__un__save__"; + + private static SerializeFilter[] serializeFilters; + + /** + * 拓展信息 + */ + private static final ThreadLocal> EXTRA_HOLDER = ThreadLocal.withInitial(Maps::newMap); + + /** + * 当前用户 优先于登录用户 + */ + private static final ThreadLocal USER_HOLDER = new ThreadLocal<>(); + + private OperatorLogs() { + } + + /** + * 添加日志参数 + * + * @param key key + * @param value value + */ + public static void add(String key, Object value) { + EXTRA_HOLDER.get().put(key, value); + } + + /** + * 添加日志参数 json + * + * @param key key + * @param value value + */ + public static void addJson(String key, Object value) { + EXTRA_HOLDER.get().put(key, JSON.parseObject(toJsonString(value))); + } + + /** + * 添加日志参数 + * + * @param map map + */ + public static void add(Map map) { + EXTRA_HOLDER.get().putAll(map); + } + + /** + * 添加日志参数 + * + * @param obj obj + */ + @SuppressWarnings("unchecked") + public static void add(Object obj) { + if (obj == null) { + return; + } +// if (obj instanceof JSONObject || obj instanceof com.alibaba.fastjson2.JSONObject) { + if (obj instanceof JSONObject || obj instanceof JSONObject) { + EXTRA_HOLDER.get().putAll(JSON.parseObject(toJsonString(obj))); + } else if (obj instanceof Map) { + EXTRA_HOLDER.get().putAll((Map) obj); + } else { + EXTRA_HOLDER.get().putAll(JSON.parseObject(toJsonString(obj))); + } + } + + /** + * 获取 json + * + * @param value value + * @return json + */ + public static String toJsonString(Object value) { + return JSON.toJSONString(value, serializeFilters); + } + + /** + * 设置不保存 + */ + public static void unSave() { + setSave(false); + } + + /** + * 设置是否保存 + * + * @param save save + */ + public static void setSave(boolean save) { + if (save) { + EXTRA_HOLDER.get().remove(UN_SAVE_FLAG); + } else { + EXTRA_HOLDER.get().put(UN_SAVE_FLAG, UN_SAVE_FLAG); + } + } + + /** + * 设置是否保存 + * + * @return save + */ + public static boolean isSave() { + return !UN_SAVE_FLAG.equals(EXTRA_HOLDER.get().get(UN_SAVE_FLAG)); + } + + /** + * 获取参数 + * + * @return map + */ + public static Map get() { + return EXTRA_HOLDER.get(); + } + + /** + * 设置用户信息 + * + * @param user user + */ + public static void setUser(LoginUser user) { + USER_HOLDER.set(user); + } + + /** + * 获取用户 + * + * @return user + */ + public static LoginUser getUser() { + return USER_HOLDER.get(); + } + + /** + * 清空 + */ + public static void clear() { + EXTRA_HOLDER.remove(); + USER_HOLDER.remove(); + } + + /** + * 清空 html tag + * + * @param log log + * @return cleared + */ + public static String clearHtmlTag(String log) { + if (Strings.isBlank(log)) { + return log; + } + return log.replaceAll("", "") + .replaceAll("", "") + .replaceAll("", "") + .replaceAll("", "") + .replaceAll("", "") + .replaceAll("", "") + .replaceAll("", "") + .replaceAll("", "") + .replaceAll("", "") + .replaceAll("", "") + .replaceAll("
", "\n"); + } + + public static void setSerializeFilters(SerializeFilter[] serializeFilters) { + if (OperatorLogs.serializeFilters != null) { + // unmodified + throw Exceptions.state(); + } + OperatorLogs.serializeFilters = serializeFilters; + } + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/OperatorRiskLevel.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/OperatorRiskLevel.java new file mode 100644 index 00000000..776eb682 --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/OperatorRiskLevel.java @@ -0,0 +1,33 @@ + +package cd.casic.module.terminal.common; + +import lombok.Getter; + +/** + * 操作风险等级 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2023/10/12 14:19 + */ +@Getter +public enum OperatorRiskLevel { + + /** + * 低风险 + */ + L, + + /** + * 中风险 + */ + M, + + /** + * 高风险 + */ + H, + + ; + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/OperatorType.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/OperatorType.java new file mode 100644 index 00000000..1de3c238 --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/OperatorType.java @@ -0,0 +1,53 @@ + +package cd.casic.module.terminal.common; + +import lombok.Getter; + +/** + * 操作类型定义 + *

+ * 因为枚举需要实现 注解中不可以使用 则需要使用对象 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2023/10/10 10:29 + */ +@Getter +public class OperatorType { + + /** + * 风险等级 + */ + private final OperatorRiskLevel riskLevel; + + /** + * 模块 + */ + private String module; + + /** + * 类型 + */ + private final String type; + + /** + * 模板 + */ + private final String template; + + public OperatorType(OperatorRiskLevel riskLevel, String type, String template) { + this(riskLevel, null, type, template); + } + + public OperatorType(OperatorRiskLevel riskLevel, String module, String type, String template) { + this.riskLevel = riskLevel; + this.module = module; + this.type = type; + this.template = template; + } + + public void setModule(String module) { + this.module = module; + } + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/OperatorTypeDefinition.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/OperatorTypeDefinition.java new file mode 100644 index 00000000..7a0544c5 --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/OperatorTypeDefinition.java @@ -0,0 +1,21 @@ + +package cd.casic.module.terminal.common; + + +/** + * 操作类型定义 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2023/10/13 18:23 + */ +public interface OperatorTypeDefinition { + + /** + * 获取操作类型 + * + * @return 操作类型 + */ + OperatorType[] types(); + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/OperatorTypeHolder.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/OperatorTypeHolder.java new file mode 100644 index 00000000..938130bc --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/OperatorTypeHolder.java @@ -0,0 +1,40 @@ + +package cd.casic.module.terminal.common; + +import java.util.HashMap; +import java.util.Map; + +/** + * 操作日志类型实例 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2023/10/10 14:43 + */ +public class OperatorTypeHolder { + + private static final Map TYPES = new HashMap<>(); + + private OperatorTypeHolder() { + } + + /** + * 获取类型 + * + * @param key key + * @return type + */ + public static OperatorType get(String key) { + return TYPES.get(key); + } + + /** + * 设置类型 + * + * @param type type + */ + public static void set(OperatorType type) { + TYPES.put(type.getType(), type); + } + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/RsaDecryptor.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/RsaDecryptor.java new file mode 100644 index 00000000..5549fd27 --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/RsaDecryptor.java @@ -0,0 +1,21 @@ + +package cd.casic.module.terminal.common; + +/** + * rsa 解密器 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2025/1/5 20:58 + */ +public interface RsaDecryptor { + + /** + * 解密 + * + * @param value value + * @return value + */ + String decrypt(String value); + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/RsaDecryptorImpl.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/RsaDecryptorImpl.java new file mode 100644 index 00000000..c614f2ff --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/RsaDecryptorImpl.java @@ -0,0 +1,36 @@ + +package cd.casic.module.terminal.common; + +import cd.casic.module.terminal.common.config.ConfigRef; +import cd.casic.module.terminal.common.config.ConfigStore; +import cd.casic.module.terminal.common.constant.ConfigKeys; +import cn.orionsec.kit.lang.utils.crypto.RSA; +import java.security.interfaces.RSAPrivateKey; +import java.util.Arrays; +import java.util.stream.Collectors; + +/** + * rsa 加密器 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2025/1/7 11:32 + */ +public class RsaDecryptorImpl implements RsaDecryptor { + + private static final String SPLIT = "\\|"; + + private final ConfigRef privateKey; + + public RsaDecryptorImpl(ConfigStore configStore) { + this.privateKey = configStore.ref(ConfigKeys.ENCRYPT_PRIVATE_KEY, RSA::getPrivateKey); + } + + @Override + public String decrypt(String value) { + return Arrays.stream(value.split(SPLIT)) + .map(s -> RSA.decrypt(s, privateKey.value)) + .collect(Collectors.joining()); + } + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/TraceIdHolder.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/TraceIdHolder.java new file mode 100644 index 00000000..7c1aa3e6 --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/TraceIdHolder.java @@ -0,0 +1,99 @@ + +package cd.casic.module.terminal.common; + +import cn.orionsec.kit.lang.id.UUIds; +import com.alibaba.ttl.TransmittableThreadLocal; +import org.slf4j.MDC; + +/** + * traceId 持有者 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2023/6/16 17:35 + */ +public class TraceIdHolder { + + public static final String TRACE_ID_HEADER = "trace-id"; + + public static final String TRACE_ID_MDC = "tid"; + + private TraceIdHolder() { + } + + /** + * 请求序列 + */ + private static final ThreadLocal HOLDER = new TransmittableThreadLocal<>(); + + /** + * 获取 traceId + * + * @return traceId + */ + public static String get() { + return HOLDER.get(); + } + + /** + * 设置 traceId + */ + public static void set() { + set(createTraceId()); + } + + /** + * 设置 traceId + * + * @param traceId traceId + */ + public static void set(String traceId) { + // 设置应用上下文 + HOLDER.set(traceId); + // 设置日志上下文 + setMdc(traceId); + } + + /** + * 删除 traceId + */ + public static void remove() { + // 移除应用上下文 + HOLDER.remove(); + // 移除日志上下文 + removeMdc(); + } + + /** + * 从应用上下文 设置到日志上下文 + */ + public static void setMdc() { + setMdc(HOLDER.get()); + } + + /** + * 设置到日志上下文 + * + * @param traceId traceId + */ + public static void setMdc(String traceId) { + MDC.put(TRACE_ID_MDC, traceId); + } + + /** + * 移除日志上下文 + */ + public static void removeMdc() { + MDC.remove(TRACE_ID_MDC); + } + + /** + * 创建 traceId + * + * @return traceId + */ + public static String createTraceId() { + return UUIds.random32(); + } + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/UpdatePasswordAction.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/UpdatePasswordAction.java new file mode 100644 index 00000000..dbce9cc8 --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/UpdatePasswordAction.java @@ -0,0 +1,36 @@ + +package cd.casic.module.terminal.common; + +import java.io.Serializable; + +/** + * 更新密码操作 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2023/9/21 11:32 + */ +public interface UpdatePasswordAction extends Serializable { + + /** + * 是否使用新密码 + * + * @return use + */ + Boolean getUseNewPassword(); + + /** + * 获取密码 + * + * @return password + */ + String getPassword(); + + /** + * 设置密码 + * + * @param password password + */ + void setPassword(String password); + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/WebSocketSyncSession.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/WebSocketSyncSession.java new file mode 100644 index 00000000..dc30b567 --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/WebSocketSyncSession.java @@ -0,0 +1,119 @@ + +package cd.casic.module.terminal.common; + +import org.springframework.http.HttpHeaders; +import org.springframework.web.socket.CloseStatus; +import org.springframework.web.socket.WebSocketExtension; +import org.springframework.web.socket.WebSocketMessage; +import org.springframework.web.socket.WebSocketSession; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.URI; +import java.security.Principal; +import java.util.List; +import java.util.Map; + +/** + * web socket 同步会话 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2024/5/20 10:12 + */ +public class WebSocketSyncSession implements WebSocketSession { + + private final WebSocketSession delegate; + + public WebSocketSyncSession(WebSocketSession delegate) { + this.delegate = delegate; + } + + @Override + public String getId() { + return this.delegate.getId(); + } + + @Override + public URI getUri() { + return this.delegate.getUri(); + } + + @Override + public HttpHeaders getHandshakeHeaders() { + return this.delegate.getHandshakeHeaders(); + } + + @Override + public Map getAttributes() { + return this.delegate.getAttributes(); + } + + @Override + public Principal getPrincipal() { + return this.delegate.getPrincipal(); + } + + @Override + public InetSocketAddress getLocalAddress() { + return this.delegate.getLocalAddress(); + } + + @Override + public InetSocketAddress getRemoteAddress() { + return this.delegate.getRemoteAddress(); + } + + @Override + public String getAcceptedProtocol() { + return this.delegate.getAcceptedProtocol(); + } + + @Override + public void setTextMessageSizeLimit(int messageSizeLimit) { + this.delegate.setTextMessageSizeLimit(messageSizeLimit); + } + + @Override + public int getTextMessageSizeLimit() { + return this.delegate.getTextMessageSizeLimit(); + } + + @Override + public void setBinaryMessageSizeLimit(int messageSizeLimit) { + this.delegate.setBinaryMessageSizeLimit(messageSizeLimit); + } + + @Override + public int getBinaryMessageSizeLimit() { + return this.delegate.getBinaryMessageSizeLimit(); + } + + @Override + public List getExtensions() { + return this.delegate.getExtensions(); + } + + @Override + public void sendMessage(WebSocketMessage message) throws IOException { + synchronized (this.delegate) { + this.delegate.sendMessage(message); + } + } + + @Override + public boolean isOpen() { + return this.delegate.isOpen(); + } + + @Override + public void close() throws IOException { + this.delegate.close(); + } + + @Override + public void close(CloseStatus status) throws IOException { + this.delegate.close(status); + } + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/WebSockets.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/WebSockets.java new file mode 100644 index 00000000..86adf457 --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/WebSockets.java @@ -0,0 +1,128 @@ + +package cd.casic.module.terminal.common; + +import cn.orionsec.kit.lang.utils.Exceptions; +import cn.orionsec.kit.lang.utils.Threads; +import com.alibaba.fastjson.JSON; +import lombok.extern.slf4j.Slf4j; + +import org.springframework.web.socket.CloseStatus; +import org.springframework.web.socket.TextMessage; +import org.springframework.web.socket.WebSocketSession; +import java.io.IOException; + +/** + * websocket 工具类 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2021/6/14 0:36 + */ +@Slf4j +public class WebSockets { + + private WebSockets() { + } + + /** + * 创建同步会话 + * + * @param session session + * @return session + */ + public static WebSocketSession createSyncSession(WebSocketSession session) { + return new WebSocketSyncSession(session); + } + + /** + * 获取属性 + * + * @param channel channel + * @param attr attr + * @param T + * @return T + */ + @SuppressWarnings("unchecked") + public static E getAttr(WebSocketSession channel, String attr) { + return (E) channel.getAttributes().get(attr); + } + + /** + * 发送消息 忽略并发报错 + * + * @param session session + * @param message message + */ + public static void sendJson(WebSocketSession session, Object message) { + sendText(session, JSON.toJSONString(message)); + } + + /** + * 发送消息 忽略并发报错 + * + * @param session session + * @param message message + */ + public static void sendText(WebSocketSession session, String message) { + if (!session.isOpen()) { + return; + } + try { + if (session instanceof WebSocketSyncSession) { + // 发送消息 + session.sendMessage(new TextMessage(message)); + } else { + synchronized (session) { + // 发送消息 + session.sendMessage(new TextMessage(message)); + } + } + } catch (IllegalStateException e) { + // 并发异常 + log.error("发送消息失败, 准备进行重试 {}", Exceptions.getDigest(e)); + // 并发重试 + retrySendText(session, message, Const.MS_100); + } catch (IOException e) { + throw Exceptions.ioRuntime(e); + } + } + + /** + * 重试发送消息 忽略并发报错 + * + * @param session session + * @param message message + * @param delay delay + */ + public static void retrySendText(WebSocketSession session, String message, long delay) { + if (!session.isOpen()) { + return; + } + try { + Threads.sleep(delay); + session.sendMessage(new TextMessage(message)); + log.info("消息重发成功"); + Threads.sleep(delay); + } catch (Exception ex) { + throw Exceptions.ioRuntime(ex); + } + } + + /** + * 关闭会话 + * + * @param session session + * @param code code + */ + public static void close(WebSocketSession session, WsCloseCode code) { + if (!session.isOpen()) { + return; + } + try { + session.close(new CloseStatus(code.getCode(), code.getReason())); + } catch (Exception e) { + log.error("websocket close failure", e); + } + } + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/WsCloseCode.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/WsCloseCode.java new file mode 100644 index 00000000..10c17b6e --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/WsCloseCode.java @@ -0,0 +1,27 @@ + +package cd.casic.module.terminal.common; + +/** + * ws 服务端关闭 code + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2021/6/16 15:18 + */ +public interface WsCloseCode { + + /** + * code + * + * @return code + */ + int getCode(); + + /** + * reason + * + * @return reason + */ + String getReason(); + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/annotation/Module.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/annotation/Module.java new file mode 100644 index 00000000..3f5e7b4c --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/annotation/Module.java @@ -0,0 +1,25 @@ +package cd.casic.module.terminal.common.annotation; + +import org.springframework.stereotype.Component; + +import java.lang.annotation.*; + +/** + * 模块 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2023/10/9 18:44 + */ +@Component +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface Module { + + /** + * 模块 + */ + String value(); + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/config/ConfigRef.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/config/ConfigRef.java new file mode 100644 index 00000000..f03f0f39 --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/config/ConfigRef.java @@ -0,0 +1,58 @@ +package cd.casic.module.terminal.common.config; + +import lombok.extern.slf4j.Slf4j; + +import java.util.function.BiConsumer; +import java.util.function.Function; + +/** + * 配置引用 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2025/1/6 18:01 + */ +@Slf4j +public abstract class ConfigRef { + + public final String key; + + public T value; + + protected final Function convert; + + public ConfigRef(String key, Function convert) { + this.key = key; + this.convert = convert; + } + + /** + * 覆盖配置 + * + * @param value value + */ + public abstract void override(String value); + + /** + * 修改配置 + * + * @param value value + */ + public abstract void set(T value); + + /** + * 获取配置 + * + * @return value + */ + public abstract T get(); + + /** + * 修改回调 + * + * @param changeEvent changeEvent + * @return this + */ + public abstract ConfigRef onChange(BiConsumer changeEvent); + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/config/ConfigRefImpl.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/config/ConfigRefImpl.java new file mode 100644 index 00000000..89afcbee --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/config/ConfigRefImpl.java @@ -0,0 +1,60 @@ + +package cd.casic.module.terminal.common.config; + +import cn.orionsec.kit.lang.utils.Objects1; +import lombok.extern.slf4j.Slf4j; + +import java.util.function.BiConsumer; +import java.util.function.Function; + +/** + * 配置引用实现类 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2025/1/14 16:10 + */ +@Slf4j +public class ConfigRefImpl extends ConfigRef { + + protected BiConsumer changeEvent; + + public ConfigRefImpl(String key, Function convert) { + super(key, convert); + } + + @Override + public void override(String value) { + try { + this.set(convert.apply(value)); + } catch (Exception e) { + log.error("ConfigRef trigger override error key: {}, value: {}", key, value, e); + } + } + + @Override + public void set(T value) { + T before = this.value; + this.value = value; + // 被修改 + if (!Objects1.eq(before, value)) { + log.info("ConfigRef changed key: {}, value: {}", key, value); + // 触发事件 + if (changeEvent != null) { + changeEvent.accept(value, before); + } + } + } + + @Override + public T get() { + return value; + } + + @Override + public ConfigRef onChange(BiConsumer changeEvent) { + this.changeEvent = changeEvent; + return this; + } + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/config/ConfigStore.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/config/ConfigStore.java new file mode 100644 index 00000000..20a6abe1 --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/config/ConfigStore.java @@ -0,0 +1,244 @@ + +package cd.casic.module.terminal.common.config; + +import java.util.function.Function; + +/** + * 配置中心 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2025/1/5 21:30 + */ +public interface ConfigStore { + + /** + * 获取 string 配置 + * + * @param key key + * @return config + */ + String getString(String key); + + /** + * 获取 string 配置 + * + * @param key key + * @param defaultValue defaultValue + * @return config + */ + String getString(String key, String defaultValue); + + /** + * 获取 int 配置 + * + * @param key key + * @return config + */ + Integer getInteger(String key); + + /** + * 获取 int 配置 + * + * @param key key + * @param defaultValue defaultValue + * @return config + */ + Integer getInteger(String key, Integer defaultValue); + + /** + * 获取 long 配置 + * + * @param key key + * @return config + */ + Long getLong(String key); + + /** + * 获取 long 配置 + * + * @param key key + * @param defaultValue defaultValue + * @return config + */ + Long getLong(String key, Long defaultValue); + + /** + * 获取 double 配置 + * + * @param key key + * @return config + */ + Double getDouble(String key); + + /** + * 获取 double 配置 + * + * @param key key + * @param defaultValue defaultValue + * @return config + */ + Double getDouble(String key, Double defaultValue); + + /** + * 获取 boolean 配置 + * + * @param key key + * @return config + */ + Boolean getBoolean(String key); + + /** + * 获取 boolean 配置 + * + * @param key key + * @param defaultValue defaultValue + * @return config + */ + Boolean getBoolean(String key, Boolean defaultValue); + + /** + * 获取配置 + * + * @param key key + * @return conf + */ + String getConfig(String key); + + /** + * 获取配置 + * + * @param key key + * @param defaultValue defaultValue + * @return conf + */ + String getConfig(String key, String defaultValue); + + /** + * 获取配置 + * + * @param key key + * @param convert convert + * @param T + * @return conf + */ + T getConfig(String key, Function convert); + + /** + * 获取配置 + * + * @param key key + * @param convert convert + * @param defaultValue defaultValue + * @param T + * @return conf + */ + T getConfig(String key, Function convert, T defaultValue); + + /** + * 获取 string 配置 + * + * @param key key + * @return ref + */ + ConfigRef string(String key); + + /** + * 获取 string 配置 + * + * @param key key + * @param defaultValue defaultValue + * @return ref + */ + ConfigRef string(String key, String defaultValue); + + /** + * 获取 int 配置 + * + * @param key key + * @return ref + */ + ConfigRef int32(String key); + + /** + * 获取 int 配置 + * + * @param key key + * @param defaultValue defaultValue + * @return ref + */ + ConfigRef int32(String key, Integer defaultValue); + + /** + * 获取 long 配置 + * + * @param key key + * @return ref + */ + ConfigRef int64(String key); + + /** + * 获取 long 配置 + * + * @param key key + * @param defaultValue defaultValue + * @return ref + */ + ConfigRef int64(String key, Long defaultValue); + + /** + * 获取 double 配置 + * + * @param key key + * @return ref + */ + ConfigRef float64(String key); + + /** + * 获取 double 配置 + * + * @param key key + * @param defaultValue defaultValue + * @return ref + */ + ConfigRef float64(String key, Double defaultValue); + + /** + * 获取 boolean 配置 + * + * @param key key + * @return ref + */ + ConfigRef bool(String key); + + /** + * 获取 boolean 配置 + * + * @param key key + * @param defaultValue defaultValue + * @return ref + */ + ConfigRef bool(String key, Boolean defaultValue); + + /** + * 获取配置 + * + * @param key key + * @param convert convert + * @param T + * @return ref + */ + ConfigRef ref(String key, Function convert); + + /** + * 获取配置 + * + * @param key key + * @param convert convert + * @param defaultValue defaultValue + * @param T + * @return ref + */ + ConfigRef ref(String key, Function convert, T defaultValue); + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/config/ManagementConfigStore.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/config/ManagementConfigStore.java new file mode 100644 index 00000000..3cba4371 --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/config/ManagementConfigStore.java @@ -0,0 +1,33 @@ + +package cd.casic.module.terminal.common.config; + +/** + * 可控配置中心 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2025/1/6 23:00 + */ +public interface ManagementConfigStore extends ConfigStore { + + /** + * 加载全部配置 + */ + void loadAllConfig(); + + /** + * 覆盖配置 + * + * @param key key + * @param value value + */ + void override(String key, String value); + + /** + * 注册 ref + * + * @param ref ref + */ + void register(ConfigRef ref); + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/config/ManagementConfigStoreImpl.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/config/ManagementConfigStoreImpl.java new file mode 100644 index 00000000..3289c18c --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/config/ManagementConfigStoreImpl.java @@ -0,0 +1,202 @@ + +package cd.casic.module.terminal.common.config; + +import cn.orionsec.kit.lang.utils.collect.Lists; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; + +/** + * 配置中心实现 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2025/1/6 17:20 + */ +@Slf4j +@Component +public class ManagementConfigStoreImpl implements ManagementConfigStore { + + private final ConcurrentHashMap configMap = new ConcurrentHashMap<>(); + + private final ConcurrentHashMap>> configRefs = new ConcurrentHashMap<>(); + + + @Override + public void loadAllConfig() { + /* configMap.putAll(service.getAllConfig());*/ + } + + @Override + public void override(String key, String value) { + log.info("ConfigStore.override key: {}, value: {}", key, value); + // 修改配置 + configMap.put(key, value); + // 修改引用 + List> refs = configRefs.get(key); + if (!Lists.isEmpty(refs)) { + refs.forEach(s -> s.override(value)); + } + } + + @Override + public void register(ConfigRef ref) { + String key = ref.key; + log.info("ConfigStore.register ref key: {}", key); + // 注册引用 + configRefs.computeIfAbsent(key, k -> new ArrayList<>()).add(ref); + } + + @Override + public String getString(String key) { + return this.getConfig(key); + } + + @Override + public String getString(String key, String defaultValue) { + return this.getConfig(key, defaultValue); + } + + @Override + public Integer getInteger(String key) { + return this.getConfig(key, Integer::valueOf, null); + } + + @Override + public Integer getInteger(String key, Integer defaultValue) { + return this.getConfig(key, Integer::valueOf, defaultValue); + } + + @Override + public Long getLong(String key) { + return this.getConfig(key, Long::valueOf, null); + } + + @Override + public Long getLong(String key, Long defaultValue) { + return this.getConfig(key, Long::valueOf, defaultValue); + } + + @Override + public Double getDouble(String key) { + return this.getConfig(key, Double::valueOf, null); + } + + @Override + public Double getDouble(String key, Double defaultValue) { + return this.getConfig(key, Double::valueOf, defaultValue); + } + + @Override + public Boolean getBoolean(String key) { + return this.getConfig(key, Boolean::valueOf, null); + } + + @Override + public Boolean getBoolean(String key, Boolean defaultValue) { + return this.getConfig(key, Boolean::valueOf, defaultValue); + } + + @Override + public String getConfig(String key) { + return this.getConfig(key, Function.identity(), null); + } + + @Override + public String getConfig(String key, String defaultValue) { + return this.getConfig(key, Function.identity(), defaultValue); + } + + @Override + public T getConfig(String key, Function convert) { + return this.getConfig(key, convert, null); + } + + @Override + public T getConfig(String key, Function convert, T defaultValue) { + // 获取配置 + String conf = configMap.get(key); + // 默认值 + if (conf == null) { + return defaultValue; + } + // 转换 + return convert.apply(conf); + } + + @Override + public ConfigRef string(String key) { + return this.ref(key, Function.identity(), null); + } + + @Override + public ConfigRef string(String key, String defaultValue) { + return this.ref(key, Function.identity(), defaultValue); + } + + @Override + public ConfigRef int32(String key) { + return this.ref(key, Integer::valueOf, null); + } + + @Override + public ConfigRef int32(String key, Integer defaultValue) { + return this.ref(key, Integer::valueOf, defaultValue); + } + + @Override + public ConfigRef int64(String key) { + return this.ref(key, Long::valueOf, null); + } + + @Override + public ConfigRef int64(String key, Long defaultValue) { + return this.ref(key, Long::valueOf, defaultValue); + } + + @Override + public ConfigRef float64(String key) { + return this.ref(key, Double::valueOf, null); + } + + @Override + public ConfigRef float64(String key, Double defaultValue) { + return this.ref(key, Double::valueOf, defaultValue); + } + + @Override + public ConfigRef bool(String key) { + return this.ref(key, Boolean::valueOf, null); + } + + @Override + public ConfigRef bool(String key, Boolean defaultValue) { + return this.ref(key, Boolean::valueOf, defaultValue); + } + + @Override + public ConfigRef ref(String key, Function convert) { + return this.ref(key, convert, null); + } + + @Override + public ConfigRef ref(String key, Function convert, T defaultValue) { + // 创建引用 + ConfigRef ref = new ConfigRefImpl<>(key, convert); + // 设置值 + String value = configMap.get(key); + if (value != null) { + ref.override(value); + } else { + ref.set(defaultValue); + } + // 注册引用 + this.register(ref); + return ref; + } + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/constant/ConfigKeys.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/constant/ConfigKeys.java new file mode 100644 index 00000000..a22c1ab5 --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/constant/ConfigKeys.java @@ -0,0 +1,35 @@ + +package cd.casic.module.terminal.common.constant; + +/** + * 配置项常量 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2025/1/14 16:15 + */ +public interface ConfigKeys { + + /** + * SFTP 文件预览大小 + */ + String SFTP_PREVIEW_SIZE = "sftp_previewSize"; + + /** + * SFTP 重复文件备份 + */ + String SFTP_UPLOAD_PRESENT_BACKUP = "sftp_uploadPresentBackup"; + + /** + * SFTP 备份文件名称 + */ + String SFTP_UPLOAD_BACKUP_FILE_NAME = "sftp_uploadBackupFileName"; + + + /** + * 加密私钥 + */ + String ENCRYPT_PRIVATE_KEY = "encrypt_privateKey"; + + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/holder/SpringHolder.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/holder/SpringHolder.java new file mode 100644 index 00000000..9067f8bd --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/common/holder/SpringHolder.java @@ -0,0 +1,132 @@ +package cd.casic.module.terminal.common.holder; + +import cd.casic.module.terminal.utils.Valid; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.BeanFactoryPostProcessor; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.context.ApplicationEvent; +import org.springframework.context.ConfigurableApplicationContext; + +/** + * + * bean 工具类 + * @author Yuru Pu + * @version 1.0 + * @since 2025/7/8 17:44 + */ +public class SpringHolder { + + private static final Logger LOGGER = LoggerFactory.getLogger(SpringHolder.class); + + private static ApplicationContext applicationContext; + + private static ConfigurableListableBeanFactory beanFactory; + + private SpringHolder() { + } + + public static class ApplicationContextAwareStore implements ApplicationContextAware, BeanFactoryPostProcessor { + + public ApplicationContextAwareStore() { + LOGGER.info("init spring holder"); + } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + LOGGER.info("inject spring holder ApplicationContext"); + SpringHolder.applicationContext = applicationContext; + } + + @Override + public void postProcessBeanFactory(ConfigurableListableBeanFactory configurableListableBeanFactory) throws BeansException { + LOGGER.info("inject spring holder BeanFactory"); + SpringHolder.beanFactory = configurableListableBeanFactory; + } + + } + + /** + * 获取上下文容器 + * + * @return ignore + */ + public static ApplicationContext getApplicationContext() { + return applicationContext; + } + + public static ConfigurableListableBeanFactory getBeanFactory() { + return beanFactory; + } + + /** + * 发布事件 + * + * @param event event + */ + public static void publishEvent(ApplicationEvent event) { + applicationContext.publishEvent(event); + } + + /** + * 发布事件 + * + * @param event event + */ + public static void publishEvent(Object event) { + applicationContext.publishEvent(event); + } + + @SuppressWarnings("unchecked") + public static T getBean(String beanName) { + return ((T) applicationContext.getBean(beanName)); + } + + public static T getBean(Class type) { + return applicationContext.getBean(type); + } + + public static T getBean(String beanName, Class type) { + return applicationContext.getBean(beanName, type); + } + + public static boolean containsBean(String beanName) { + return applicationContext.containsBean(beanName); + } + + public static String[] getBeanNames() { + return applicationContext.getBeanDefinitionNames(); + } + + public static boolean isSingletonBean(String beanName) { + return applicationContext.isSingleton(beanName); + } + + public static Class getType(String beanName) { + return applicationContext.getType(beanName); + } + + public static boolean isType(String beanName, Class beanType) { + return applicationContext.isTypeMatch(beanName, beanType); + } + + public static String[] getBeanAliases(String beanName) { + return applicationContext.getAliases(beanName); + } + + public static void close() { + if (applicationContext instanceof ConfigurableApplicationContext) { + ((ConfigurableApplicationContext) applicationContext).close(); + } + } + + public static void refresh() { + Valid.isInstanceOf(applicationContext, ConfigurableApplicationContext.class); + ConfigurableApplicationContext applicationContext = (ConfigurableApplicationContext) SpringHolder.applicationContext; + applicationContext.refresh(); + } + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/configuration/SpringConfiguration.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/configuration/SpringConfiguration.java new file mode 100644 index 00000000..121db19b --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/configuration/SpringConfiguration.java @@ -0,0 +1,26 @@ +package cd.casic.module.terminal.configuration; + +import cd.casic.module.terminal.common.holder.SpringHolder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * + * spring 配置类 + * + * @author Yuru Pu + * @version 1.0 + * @since 2025/7/10 14:46 + */ +@Configuration +public class SpringConfiguration { + + /** + * @return spring 容器工具类 + */ + @Bean + public SpringHolder.ApplicationContextAwareStore springHolderAware() { + return new SpringHolder.ApplicationContextAwareStore(); + } + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/controller/TerminalController.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/controller/TerminalController.java new file mode 100644 index 00000000..1e893d80 --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/controller/TerminalController.java @@ -0,0 +1,46 @@ +package cd.casic.module.terminal.controller; + +import cd.casic.framework.commons.pojo.CommonResult; +import cd.casic.module.terminal.service.impl.TerminalService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import static cd.casic.framework.commons.pojo.CommonResult.success; + +/** + * + * 终端连接 + * @author Yuru Pu + * @version 1.0 + * @since 2025/7/8 11:36 + */ +@Tag(name = "管理后台 - 终端") +@RestController +@RequestMapping("/asset/terminal") +@Validated +public class TerminalController { + + @Resource + private TerminalService terminalService; + + @GetMapping("/access") + @Operation(summary = "获取终端key") + @PreAuthorize("@ss.hasPermission('terminal:access')") + public CommonResult getTerminalAccessToken() { + return success(terminalService.getTerminalAccessToken()); + } + + @GetMapping("/transfer") + @Operation(summary = "获取终端 transferToken") + @PreAuthorize("@ss.hasPermission('asset:terminal:access')") + public CommonResult getTerminalTransferToken() { + return success(terminalService.getTerminalTransferToken()); + } + +} + diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/controller/TerminalSftpController.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/controller/TerminalSftpController.java new file mode 100644 index 00000000..873ac566 --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/controller/TerminalSftpController.java @@ -0,0 +1,63 @@ +package cd.casic.module.terminal.controller; + +import cd.casic.module.terminal.service.TerminalSftpService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.annotation.security.PermitAll; +import lombok.extern.slf4j.Slf4j; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; +import jakarta.servlet.http.HttpServletResponse; + +/** + * SFTP 操作服务 api + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2023-12-26 22:09 + */ +@Tag(name = "asset - SFTP 操作服务") +@Slf4j +@Validated +@RestController +@RequestMapping("/asset/terminal-sftp") +public class TerminalSftpController { + + @Resource + private TerminalSftpService terminalSftpService; + + + @GetMapping("/get-content") + @Operation(summary = "获取文件内容") + @Parameter(name = "token", description = "token", required = true) + public void getFileContentByToken(@RequestParam("token") String token, HttpServletResponse response) throws Exception { + terminalSftpService.getFileContentByToken(token, response); + } + + @PostMapping("/set-content") + @Operation(summary = "设置文件内容") + @Parameter(name = "token", description = "token", required = true) + @Parameter(name = "file", description = "file", required = true) + public Boolean setFileContentByToken(@RequestParam("token") String token, + @RequestParam("file") MultipartFile file) throws Exception { + terminalSftpService.setFileContentByToken(token, file); + return true; + } + + @PermitAll + @GetMapping("/download") + @Operation(summary = "下载文件") + @Parameter(name = "channelId", description = "channelId", required = true) + @Parameter(name = "transferToken", description = "transferToken", required = true) + public StreamingResponseBody downloadWithTransferToken(@RequestParam("channelId") String channelId, + @RequestParam("transferToken") String transferToken, + HttpServletResponse response) { + return terminalSftpService.downloadWithTransferToken(channelId, transferToken, response); + } + +} + diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/controller/dto/TerminalAccessDTO.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/controller/dto/TerminalAccessDTO.java new file mode 100644 index 00000000..867c5795 --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/controller/dto/TerminalAccessDTO.java @@ -0,0 +1,31 @@ +package cd.casic.module.terminal.controller.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + + +/** + * 终端访问参数 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2023/12/26 15:47 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +//@DesensitizeObject 、、todo 自动脱敏的注解 +@Schema(name = "TerminalAccessDTO", description = "终端访问参数") +public class TerminalAccessDTO { + + @Schema(description = "userId") + private Long userId; + + @Schema(description = "username") + private String username; + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/controller/dto/TerminalConnectDTO.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/controller/dto/TerminalConnectDTO.java new file mode 100644 index 00000000..6ef29bd6 --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/controller/dto/TerminalConnectDTO.java @@ -0,0 +1,86 @@ +package cd.casic.module.terminal.controller.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + + +/** + * 终端连接参数 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2023/12/26 15:47 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +//@DesensitizeObject +@Schema(name = "TerminalConnectDTO", description = "终端连接参数") +public class TerminalConnectDTO { + + @Schema(description = "logId") + private Long logId; + + @Schema(description = "连接类型") + private String connectType; + + @Schema(description = "hostId") + private Long hostId; + + @Schema(description = "hostName") + private String hostName; + + @Schema(description = "主机编码") + private String hostCode; + + @Schema(description = "主机地址") + private String hostAddress; + + @Schema(description = "主机端口") + private Integer hostPort; + + @Schema(description = "系统类型") + private String osType; + + @Schema(description = "系统架构") + private String archType; + + @Schema(description = "超时时间") + private Integer timeout; + + @Schema(description = "SSH输出编码") + private String charset; + + @Schema(description = "文件名称编码") + private String fileNameCharset; + + @Schema(description = "文件内容编码") + private String fileContentCharset; + + @Schema(description = "用户名") + private String username; + +// @Desensitize(toEmpty = true) + @Schema(description = "密码") + private String password; + + @Schema(description = "密钥id") + private Long keyId; + +// @Desensitize(toEmpty = true) + @Schema(description = "公钥文本") + private String publicKey; + +// @Desensitize(toEmpty = true) + @Schema(description = "私钥文本") + private String privateKey; + +// @Desensitize(toEmpty = true) + @Schema(description = "私钥密码") + private String privateKeyPassword; + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/controller/dto/TerminalTransferDTO.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/controller/dto/TerminalTransferDTO.java new file mode 100644 index 00000000..587c5dc6 --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/controller/dto/TerminalTransferDTO.java @@ -0,0 +1,30 @@ +package cd.casic.module.terminal.controller.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 终端传输参数 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2023/12/26 15:47 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +//@DesensitizeObject +@Schema(name = "TerminalTransferDTO", description = "终端传输参数") +public class TerminalTransferDTO { + + @Schema(description = "userId") + private Long userId; + + @Schema(description = "username") + private String username; + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/controller/request/TerminalConnectLogQueryRequest.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/controller/request/TerminalConnectLogQueryRequest.java new file mode 100644 index 00000000..094133c1 --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/controller/request/TerminalConnectLogQueryRequest.java @@ -0,0 +1,67 @@ +package cd.casic.module.terminal.controller.request; + +import com.fasterxml.jackson.annotation.JsonFormat; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.*; +import org.springframework.data.annotation.Id; +import java.util.Date; +import java.util.List; + +/** + * + * 终端连接日志 查询请求对象 + * + * @author Yuru Pu + * @version 1.0 + * @since 2025/7/9 14:07 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Schema(name = "TerminalConnectLogQueryRequest", description = "终端连接日志 查询请求对象") +public class TerminalConnectLogQueryRequest { + + @NotNull(groups = Id.class) + @Schema(description = "id") + private Long id; + + @Schema(description = "id") + private List idList; + + @Schema(description = "用户id") + private Long userId; + + @Schema(description = "主机id") + private Long hostId; + + @Size(max = 128) + @Schema(description = "主机地址") + private String hostAddress; + + @Size(max = 16) + @Schema(description = "类型") + private String type; + + @Size(max = 64) + @Schema(description = "sessionId") + private String sessionId; + + @Size(max = 16) + @Schema(description = "状态") + private String status; + + @Schema(description = "开始时间-区间") + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") + private Date[] startTimeRange; + + @Schema(description = "创建时间 <=") + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") + private Date createTimeLe; + + @Schema(description = "状态") + private List statusList; + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/controller/vo/TerminalConnectLogVO.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/controller/vo/TerminalConnectLogVO.java new file mode 100644 index 00000000..8ab89b6a --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/controller/vo/TerminalConnectLogVO.java @@ -0,0 +1,63 @@ +package cd.casic.module.terminal.controller.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import java.io.Serializable; +import java.util.Date; + +/** + * 终端连接日志 视图响应对象 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2023-12-26 22:09 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Schema(name = "TerminalConnectLogVO", description = "终端连接日志 视图响应对象") +public class TerminalConnectLogVO implements Serializable { + + private static final long serialVersionUID = 1L; + + @Schema(description = "id") + private Long id; + + @Schema(description = "用户id") + private Long userId; + + @Schema(description = "用户名") + private String username; + + @Schema(description = "主机id") + private Long hostId; + + @Schema(description = "主机名称") + private String hostName; + + @Schema(description = "主机地址") + private String hostAddress; + + @Schema(description = "类型") + private String type; + + @Schema(description = "状态") + private String status; + + @Schema(description = "sessionId") + private String sessionId; + + @Schema(description = "开始时间") + private Date startTime; + + @Schema(description = "结束时间") + private Date endTime; + + /*@Schema(description = "额外信息") + private TerminalConnectLogExtraDTO extra;*/ + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/convert/TerminalConnectLogConvert.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/convert/TerminalConnectLogConvert.java new file mode 100644 index 00000000..b9249461 --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/convert/TerminalConnectLogConvert.java @@ -0,0 +1,37 @@ +package cd.casic.module.terminal.convert; + + +import cd.casic.module.terminal.dal.dataobject.TerminalConnectLogDO; +import cd.casic.module.terminal.controller.dto.TerminalConnectDTO; +import cd.casic.module.terminal.controller.request.TerminalConnectLogQueryRequest; +import cd.casic.module.terminal.controller.vo.TerminalConnectLogVO; +import cd.casic.module.terminal.host.terminal.model.request.host.TerminalConnectLogCreateRequest; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; +import java.util.List; + + +/** + * + * 终端连接日志 内部对象转换器 + * + * @author Yuru Pu + * @version 1.0 + * @since 2025/7/8 13:54 + */ +@Mapper +public interface TerminalConnectLogConvert { + + TerminalConnectLogConvert MAPPER = Mappers.getMapper(TerminalConnectLogConvert.class); + + TerminalConnectLogDO to(TerminalConnectLogCreateRequest request); + + TerminalConnectLogDO to(TerminalConnectLogQueryRequest request); + + TerminalConnectLogVO to(TerminalConnectLogDO domain); + + TerminalConnectLogCreateRequest to(TerminalConnectDTO dto); + + List to(List list); + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/dal/dataobject/TerminalConnectLogDO.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/dal/dataobject/TerminalConnectLogDO.java new file mode 100644 index 00000000..564673fb --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/dal/dataobject/TerminalConnectLogDO.java @@ -0,0 +1,76 @@ +package cd.casic.module.terminal.dal.dataobject; + +import cd.casic.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.*; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.util.Date; + +/** + * 终端连接日志 实体对象 + * + * @author Yuru Pu + * @version 1.0 + * @since 2025/7/9 11:33 + */ +@Data +@TableName(value = "terminal_connect_log", autoResultMap = true) +@KeySequence("terminal_connect_log_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Accessors(chain = true) +@EqualsAndHashCode(callSuper = true) +public class TerminalConnectLogDO extends BaseDO { + + + @TableId + private Long id; + + @Schema(description = "用户id") + @TableField("user_id") + private Long userId; + + @Schema(description = "用户名") + @TableField("username") + private String username; + + @Schema(description = "主机id") + @TableField("host_id") + private Long hostId; + + @Schema(description = "主机名称") + @TableField("host_name") + private String hostName; + + @Schema(description = "主机地址") + @TableField("host_address") + private String hostAddress; + + @Schema(description = "类型") + @TableField("type") + private String type; + + @Schema(description = "sessionId") + @TableField("session_id") + private String sessionId; + + @Schema(description = "状态") + @TableField("status") + private String status; + + @Schema(description = "开始时间") + @TableField("start_time") + private Date startTime; + + @Schema(description = "结束时间") + @TableField("end_time") + private Date endTime; + + @Schema(description = "额外信息") + @TableField("extra_info") + private String extraInfo; + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/dal/mysql/TerminalConnectLogMapper.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/dal/mysql/TerminalConnectLogMapper.java new file mode 100644 index 00000000..48eb8e4a --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/dal/mysql/TerminalConnectLogMapper.java @@ -0,0 +1,18 @@ +package cd.casic.module.terminal.dal.mysql; + +import cd.casic.framework.mybatis.core.mapper.BaseMapperX; +import cd.casic.module.terminal.dal.dataobject.TerminalConnectLogDO; +import org.apache.ibatis.annotations.Mapper; + +/** + * + * 终端连接日志 Mapper 接口 + * + * @author Yuru Pu + * @version 1.0 + * @since 2025/7/9 11:38 + */ +@Mapper +public interface TerminalConnectLogMapper extends BaseMapperX { + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/dal/redis/TerminalRedisDAO.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/dal/redis/TerminalRedisDAO.java new file mode 100644 index 00000000..adbabcc7 --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/dal/redis/TerminalRedisDAO.java @@ -0,0 +1,111 @@ +package cd.casic.module.terminal.dal.redis; + +import cn.orionsec.kit.lang.define.cache.key.CacheKeyDefine; +import cn.orionsec.kit.lang.utils.Strings; +import com.alibaba.fastjson.JSON; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Repository; + +/** + * @author: Paul + * @create: 2025-07-10 15:31 + */ +@Repository +public class TerminalRedisDAO { + + + @Autowired + private StringRedisTemplate redisTemplate; + + /** + * 获取 json + * + * @param key key + * @param define define + * @param T + * @return T + */ + public T getJson(String key, CacheKeyDefine define) { + return (T) getJson(key, define.getType()); + } + + /** + * 获取 json + * + * @param key key + * @param type type + * @param T + * @return T + */ + public T getJson(String key, Class type) { + String value = redisTemplate.opsForValue().get(key); + if (value == null) { + return null; + } + return (T) JSON.parseObject(value, type); + } + + + /** + * 设置值 + * + * @param define define + * @param value value + */ + public void set(CacheKeyDefine define, Object value) { + set(define.getKey(), define, value); + } + + /** + * 设置值 + * + * @param key key + * @param define define + * @param value value + */ + public void set(String key, CacheKeyDefine define, Object value) { + if (value == null) { + value = Strings.EMPTY; + } + if (define == null || define.getTimeout() == 0) { + // 不过期 + redisTemplate.opsForValue().set(key, value.toString()); + } else { + // 过期 + redisTemplate.opsForValue().set(key, value.toString(), + define.getTimeout(), + define.getUnit()); + + } + } + + /** + * 设置 json + * + * @param key key + * @param define define + * @param value value + */ + public void setJson(String key, CacheKeyDefine define, Object value) { + if (define == null || define.getTimeout() == 0) { + // 不过期 + redisTemplate.opsForValue().set(key, JSON.toJSONString(value)); + } else { + // 过期 + redisTemplate.opsForValue().set(key, JSON.toJSONString(value), + define.getTimeout(), + define.getUnit()); + } + } + + /** + * 删除 key + * + * @param key key + */ + public void delete(String key) { + redisTemplate.delete(key); + } + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/define/AssetThreadPools.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/define/AssetThreadPools.java new file mode 100644 index 00000000..b2e222a6 --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/define/AssetThreadPools.java @@ -0,0 +1,110 @@ +package cd.casic.module.terminal.define; + +import cn.orionsec.kit.lang.constant.Const; +import cn.orionsec.kit.lang.define.thread.ExecutorBuilder; +import java.util.concurrent.SynchronousQueue; +import java.util.concurrent.ThreadPoolExecutor; + +/** + * 资产线程池 + * + */ +public interface AssetThreadPools { + + /** + * 超时检查线程池 + */ + ThreadPoolExecutor TIMEOUT_CHECK = ExecutorBuilder.create() + .namedThreadFactory("timeout-check-") + .corePoolSize(1) + .maxPoolSize(Integer.MAX_VALUE) + .keepAliveTime(Const.MS_S_60) + .workQueue(new SynchronousQueue<>()) + .allowCoreThreadTimeout(true) + .build(); + + /** + * terminal 标准输出线程池 + */ + ThreadPoolExecutor TERMINAL_STDOUT = ExecutorBuilder.create() + .namedThreadFactory("terminal-stdout-") + .corePoolSize(1) + .maxPoolSize(Integer.MAX_VALUE) + .keepAliveTime(Const.MS_S_60) + .workQueue(new SynchronousQueue<>()) + .allowCoreThreadTimeout(true) + .build(); + + /** + * terminal 操作线程池 + */ + ThreadPoolExecutor TERMINAL_OPERATOR = ExecutorBuilder.create() + .namedThreadFactory("terminal-operator-") + .corePoolSize(1) + .maxPoolSize(Integer.MAX_VALUE) + .keepAliveTime(Const.MS_S_60) + .workQueue(new SynchronousQueue<>()) + .allowCoreThreadTimeout(true) + .build(); + + /** + * 批量执行任务线程池 + */ + ThreadPoolExecutor EXEC_TASK = ExecutorBuilder.create() + .namedThreadFactory("exec-task-") + .corePoolSize(1) + .maxPoolSize(Integer.MAX_VALUE) + .keepAliveTime(Const.MS_S_60) + .workQueue(new SynchronousQueue<>()) + .allowCoreThreadTimeout(true) + .build(); + + /** + * 批量执行主机命令线程池 + */ + ThreadPoolExecutor EXEC_HOST = ExecutorBuilder.create() + .namedThreadFactory("exec-host-") + .corePoolSize(1) + .maxPoolSize(Integer.MAX_VALUE) + .keepAliveTime(Const.MS_S_60) + .workQueue(new SynchronousQueue<>()) + .allowCoreThreadTimeout(true) + .build(); + + /** + * 批量执行日志查看线程池 + */ + ThreadPoolExecutor EXEC_LOG = ExecutorBuilder.create() + .namedThreadFactory("exec-log-") + .corePoolSize(1) + .maxPoolSize(Integer.MAX_VALUE) + .keepAliveTime(Const.MS_S_60) + .workQueue(new SynchronousQueue<>()) + .allowCoreThreadTimeout(true) + .build(); + + /** + * 批量上传任务线程池 + */ + ThreadPoolExecutor UPLOAD_TASK = ExecutorBuilder.create() + .namedThreadFactory("upload-task-") + .corePoolSize(1) + .maxPoolSize(Integer.MAX_VALUE) + .keepAliveTime(Const.MS_S_60) + .workQueue(new SynchronousQueue<>()) + .allowCoreThreadTimeout(true) + .build(); + + /** + * 批量上传主机线程池 + */ + ThreadPoolExecutor UPLOAD_HOST = ExecutorBuilder.create() + .namedThreadFactory("upload-host-") + .corePoolSize(1) + .maxPoolSize(Integer.MAX_VALUE) + .keepAliveTime(Const.MS_S_60) + .workQueue(new SynchronousQueue<>()) + .allowCoreThreadTimeout(true) + .build(); + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/define/cache/TerminalCacheKeyDefine.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/define/cache/TerminalCacheKeyDefine.java new file mode 100644 index 00000000..79514c21 --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/define/cache/TerminalCacheKeyDefine.java @@ -0,0 +1,54 @@ +package cd.casic.module.terminal.define.cache; + +import cd.casic.module.terminal.controller.dto.TerminalAccessDTO; +import cd.casic.module.terminal.controller.dto.TerminalTransferDTO; +import cd.casic.module.terminal.host.terminal.dto.SftpGetContentCacheDTO; +import cd.casic.module.terminal.host.terminal.dto.SftpSetContentCacheDTO; +import cn.orionsec.kit.lang.define.cache.key.CacheKeyBuilder; +import cn.orionsec.kit.lang.define.cache.key.CacheKeyDefine; +import cn.orionsec.kit.lang.define.cache.key.struct.RedisCacheStruct; +import java.util.concurrent.TimeUnit; + +/** + * + * 终端服务缓存 key + * + * @author Yuru Pu + * @version 1.0 + * @since 2025/7/9 14:17 + */ +public interface TerminalCacheKeyDefine { + + CacheKeyDefine TERMINAL_ACCESS = new CacheKeyBuilder() + .key("terminal:access:{}") + .desc("终端访问token ${token}") + .type(TerminalAccessDTO.class) + .struct(RedisCacheStruct.STRING) + .timeout(3, TimeUnit.MINUTES) + .build(); + + CacheKeyDefine TERMINAL_TRANSFER = new CacheKeyBuilder() + .key("terminal:transfer:{}") + .desc("终端传输token ${token}") + .type(TerminalTransferDTO.class) + .struct(RedisCacheStruct.STRING) + .timeout(3, TimeUnit.MINUTES) + .build(); + + CacheKeyDefine TERMINAL_SFTP_GET_CONTENT = new CacheKeyBuilder() + .key("terminal:sftp:gc:{}") + .desc("sftp 获取文件内容 ${token}") + .type(SftpGetContentCacheDTO.class) + .struct(RedisCacheStruct.STRING) + .timeout(5, TimeUnit.MINUTES) + .build(); + + CacheKeyDefine TERMINAL_SFTP_SET_CONTENT = new CacheKeyBuilder() + .key("terminal:sftp:sc:{}") + .desc("sftp 设置文件内容 ${token}") + .type(SftpSetContentCacheDTO.class) + .struct(RedisCacheStruct.STRING) + .timeout(5, TimeUnit.MINUTES) + .build(); + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/define/config/AppSftpConfig.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/define/config/AppSftpConfig.java new file mode 100644 index 00000000..24beebb2 --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/define/config/AppSftpConfig.java @@ -0,0 +1,51 @@ +package cd.casic.module.terminal.define.config; + +import cd.casic.module.terminal.common.config.ConfigRef; +import cd.casic.module.terminal.common.config.ConfigStore; +import cd.casic.module.terminal.common.constant.ConfigKeys; +import org.springframework.stereotype.Component; + +/** + * 应用 sftp 配置 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2024/4/15 22:00 + */ +@Component +public class AppSftpConfig { + + /** + * 文件预览大小 + */ + private final ConfigRef previewSize; + + /** + * 重复文件备份 + */ + private final ConfigRef uploadPresentBackup; + + /** + * 备份文件名称 + */ + private final ConfigRef uploadBackupFileName; + + public AppSftpConfig(ConfigStore configStore) { + this.previewSize = configStore.int32(ConfigKeys.SFTP_PREVIEW_SIZE); + this.uploadPresentBackup = configStore.bool(ConfigKeys.SFTP_UPLOAD_PRESENT_BACKUP); + this.uploadBackupFileName = configStore.string(ConfigKeys.SFTP_UPLOAD_BACKUP_FILE_NAME); + } + + public Integer getPreviewSize() { + return previewSize.value; + } + + public Boolean getUploadPresentBackup() { + return uploadPresentBackup.value; + } + + public String getUploadBackupFileName() { + return uploadBackupFileName.value; + } + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/define/operator/TerminalOperatorType.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/define/operator/TerminalOperatorType.java new file mode 100644 index 00000000..385a0e19 --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/define/operator/TerminalOperatorType.java @@ -0,0 +1,69 @@ +package cd.casic.module.terminal.define.operator; + +import cd.casic.module.terminal.common.annotation.Module; +import cd.casic.module.terminal.common.InitializingOperatorTypes; +import cd.casic.module.terminal.common.OperatorType; +import cn.orionsec.kit.lang.utils.collect.Lists; +import java.util.List; +import static cd.casic.module.terminal.common.OperatorRiskLevel.*; + + +/** + * 终端 操作日志类型 + * + */ +@Module("asset:terminal") +public class TerminalOperatorType extends InitializingOperatorTypes { + + public static final String CONNECT = "terminal:connect"; + + public static final String DELETE_SFTP_LOG = "terminal:delete-sftp-log"; + + public static final String SFTP_MKDIR = "terminal:sftp-mkdir"; + + public static final String SFTP_TOUCH = "terminal:sftp-touch"; + + public static final String SFTP_MOVE = "terminal:sftp-move"; + + public static final String SFTP_REMOVE = "terminal:sftp-remove"; + + public static final String SFTP_TRUNCATE = "terminal:sftp-truncate"; + + public static final String SFTP_CHMOD = "terminal:sftp-chmod"; + + public static final String SFTP_SET_CONTENT = "terminal:sftp-set-content"; + + public static final String SFTP_UPLOAD = "terminal:sftp-upload"; + + public static final String SFTP_DOWNLOAD = "terminal:sftp-download"; + + public static final List SFTP_TYPES = Lists.of( + SFTP_MKDIR, + SFTP_TOUCH, + SFTP_MOVE, + SFTP_REMOVE, + SFTP_TRUNCATE, + SFTP_CHMOD, + SFTP_SET_CONTENT, + SFTP_UPLOAD, + SFTP_DOWNLOAD + ); + + @Override + public OperatorType[] types() { + return new OperatorType[]{ + new OperatorType(L, CONNECT, "连接主机 ${connectType} ${hostName}"), + new OperatorType(H, DELETE_SFTP_LOG, "删除 SFTP 操作日志 ${count} 条"), + new OperatorType(L, SFTP_MKDIR, "创建文件夹 ${hostName} ${path}"), + new OperatorType(L, SFTP_TOUCH, "创建文件 ${hostName} ${path}"), + new OperatorType(M, SFTP_MOVE, "移动文件 ${hostName} ${path}${target}"), + new OperatorType(H, SFTP_REMOVE, "删除文件 ${hostName} ${path}"), + new OperatorType(H, SFTP_TRUNCATE, "截断文件 ${hostName} ${path}"), + new OperatorType(M, SFTP_CHMOD, "文件提权 ${hostName} ${path} ${mod}"), + new OperatorType(M, SFTP_SET_CONTENT, "修改文件内容 ${hostName} ${path}"), + new OperatorType(M, SFTP_UPLOAD, "上传文件 ${hostName} ${path}"), + new OperatorType(M, SFTP_DOWNLOAD, "下载文件 ${hostName} ${path}"), + }; + } + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/enums/HostExtraItemEnum.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/enums/HostExtraItemEnum.java new file mode 100644 index 00000000..af7be7d7 --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/enums/HostExtraItemEnum.java @@ -0,0 +1,54 @@ +package cd.casic.module.terminal.enums; + +import cd.casic.module.terminal.common.GenericsDataModel; +import cd.casic.module.terminal.common.GenericsDataStrategy; +import cd.casic.module.terminal.common.GenericsStrategyDefinition; +import cd.casic.module.terminal.host.extra.strategy.HostLabelExtraStrategy; +import cd.casic.module.terminal.host.extra.strategy.HostSpecExtraStrategy; +import lombok.AllArgsConstructor; +import lombok.Getter; +/** + * 主机额外配置项枚举 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2023/12/20 22:48 + */ +@Getter +@AllArgsConstructor +public enum HostExtraItemEnum implements GenericsStrategyDefinition { + + /** + * SSH 额外配置 + */ +// SSH(HostSshExtraStrategy.class, true), + + /** + * 标签额外配置 + */ + LABEL(HostLabelExtraStrategy.class, true), + + /** + * 规格信息配置 + */ + SPEC(HostSpecExtraStrategy.class, false), + + ; + + private final Class> strategyClass; + + private final boolean userExtra; + + public static HostExtraItemEnum of(String item) { + if (item == null) { + return null; + } + for (HostExtraItemEnum value : values()) { + if (value.name().equals(item)) { + return value; + } + } + return null; + } + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/enums/HostExtraSshAuthTypeEnum.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/enums/HostExtraSshAuthTypeEnum.java new file mode 100644 index 00000000..55bac265 --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/enums/HostExtraSshAuthTypeEnum.java @@ -0,0 +1,41 @@ +package cd.casic.module.terminal.enums; + +/** + * 主机认证类型 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2023/12/20 21:41 + */ +public enum HostExtraSshAuthTypeEnum { + + /** + * 默认认证方式 + */ + DEFAULT, + + /** + * 自定义密钥认证 + */ + CUSTOM_KEY, + + /** + * 自定义身份认证 + */ + CUSTOM_IDENTITY, + + ; + + public static HostExtraSshAuthTypeEnum of(String type) { + if (type == null) { + return DEFAULT; + } + for (HostExtraSshAuthTypeEnum value : values()) { + if (value.name().equals(type)) { + return value; + } + } + return DEFAULT; + } + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/enums/HostIdentityTypeEnum.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/enums/HostIdentityTypeEnum.java new file mode 100644 index 00000000..5a612fc4 --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/enums/HostIdentityTypeEnum.java @@ -0,0 +1,36 @@ +package cd.casic.module.terminal.enums; + +/** + * 主机身份类型 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2023/9/21 19:01 + */ +public enum HostIdentityTypeEnum { + + /** + * 密码 + */ + PASSWORD, + + /** + * 密钥 + */ + KEY, + + ; + + public static HostIdentityTypeEnum of(String type) { + if (type == null) { + return null; + } + for (HostIdentityTypeEnum value : values()) { + if (value.name().equals(type)) { + return value; + } + } + return null; + } + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/enums/HostSshAuthTypeEnum.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/enums/HostSshAuthTypeEnum.java new file mode 100644 index 00000000..26805910 --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/enums/HostSshAuthTypeEnum.java @@ -0,0 +1,41 @@ +package cd.casic.module.terminal.enums; + +/** + * 主机认证类型 - ssh + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2023/9/21 19:01 + */ +public enum HostSshAuthTypeEnum { + + /** + * 密码认证 + */ + PASSWORD, + + /** + * 密钥认证 + */ + KEY, + + /** + * 身份认证 + */ + IDENTITY, + + ; + + public static HostSshAuthTypeEnum of(String type) { + if (type == null) { + return PASSWORD; + } + for (HostSshAuthTypeEnum value : values()) { + if (value.name().equals(type)) { + return value; + } + } + return PASSWORD; + } + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/enums/HostStatusEnum.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/enums/HostStatusEnum.java new file mode 100644 index 00000000..9f0b7cb9 --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/enums/HostStatusEnum.java @@ -0,0 +1,36 @@ +package cd.casic.module.terminal.enums; + +/** + * 主机状态 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2024/7/17 16:07 + */ +public enum HostStatusEnum { + + /** + * 停用 + */ + DISABLED, + + /** + * 启用 + */ + ENABLED, + + ; + + public static HostStatusEnum of(String name) { + if (name == null) { + return null; + } + for (HostStatusEnum value : values()) { + if (value.name().equals(name)) { + return value; + } + } + return null; + } + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/enums/HostTypeEnum.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/enums/HostTypeEnum.java new file mode 100644 index 00000000..cbf6e47b --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/enums/HostTypeEnum.java @@ -0,0 +1,48 @@ +package cd.casic.module.terminal.enums; + +import cd.casic.module.terminal.common.GenericsDataModel; +import cd.casic.module.terminal.common.GenericsDataStrategy; +import cd.casic.module.terminal.common.GenericsStrategyDefinition; +import cd.casic.module.terminal.host.config.strategy.HostSshConfigStrategy; +import cn.orionsec.kit.lang.constant.Const; +import lombok.AllArgsConstructor; +import lombok.Getter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +/** + * 主机配置类型枚举 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2023/9/11 14:37 + */ +@Getter +@AllArgsConstructor +public enum HostTypeEnum implements GenericsStrategyDefinition { + + /** + * SSH + */ + SSH(HostSshConfigStrategy.class), + + ; + + private final Class> strategyClass; + + public static HostTypeEnum of(String type) { + if (type == null) { + return null; + } + for (HostTypeEnum value : values()) { + if (value.name().equals(type)) { + return value; + } + } + return null; + } + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/enums/TerminalConnectTypeEnum.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/enums/TerminalConnectTypeEnum.java new file mode 100644 index 00000000..88001b5a --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/enums/TerminalConnectTypeEnum.java @@ -0,0 +1,36 @@ +package cd.casic.module.terminal.enums; + +/** + * 终端连接类型 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2023/12/26 22:27 + */ +public enum TerminalConnectTypeEnum { + + /** + * ssh + */ + SSH, + + /** + * sftp + */ + SFTP, + + ; + + public static TerminalConnectTypeEnum of(String type) { + if (type == null) { + return null; + } + for (TerminalConnectTypeEnum value : values()) { + if (value.name().equalsIgnoreCase(type)) { + return value; + } + } + return null; + } + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/config/model/HostSshConfigModel.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/config/model/HostSshConfigModel.java new file mode 100644 index 00000000..ce78b9da --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/config/model/HostSshConfigModel.java @@ -0,0 +1,100 @@ +package cd.casic.module.terminal.host.config.model; + +import cd.casic.module.terminal.common.GenericsDataModel; +import cd.casic.module.terminal.common.UpdatePasswordAction; +import jakarta.validation.constraints.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * + * 主机 SSH 配置 + * + * @author Yuru Pu + * @version 1.0 + * @since 2025/7/15 15:22 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class HostSshConfigModel implements GenericsDataModel, UpdatePasswordAction { + + /** + * 主机端口 + */ + @NotNull + @Min(value = 1) + @Max(value = 65535) + private Integer port; + + /** + * 用户名 + */ + @Size(max = 128) + private String username; + + /** + * 认证方式 + */ + @NotBlank + @Size(max = 12) + private String authType; + + /** + * 密码 + */ + private String password; + + /** + * 身份id + */ + private Long identityId; + + /** + * 密钥id + */ + private Long keyId; + + /** + * 连接超时时间 + */ + @NotNull + @Min(value = 1) + @Max(value = 100000) + private Integer connectTimeout; + + /** + * SSH输出编码 + */ + @NotBlank + @Size(max = 12) + private String charset; + + /** + * 文件名称编码 + */ + @NotBlank + @Size(max = 12) + private String fileNameCharset; + + /** + * 文件内容编码 + */ + @NotBlank + @Size(max = 12) + private String fileContentCharset; + + /** + * 是否使用新密码 仅参数 + */ + private Boolean useNewPassword; + + /** + * 是否已设置密码 仅返回 + */ + private Boolean hasPassword; + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/config/strategy/HostSshConfigStrategy.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/config/strategy/HostSshConfigStrategy.java new file mode 100644 index 00000000..892e1514 --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/config/strategy/HostSshConfigStrategy.java @@ -0,0 +1,128 @@ +package cd.casic.module.terminal.host.config.strategy; + +import cd.casic.module.machine.dal.mysql.MachineInfoMapper; +import cd.casic.module.machine.dal.mysql.SecretKeyMapper; +import cd.casic.module.terminal.common.AbstractGenericsDataStrategy; +import cd.casic.module.terminal.common.AesEncryptUtils; +import cd.casic.module.terminal.common.ErrorMessage; +import cd.casic.module.terminal.enums.HostSshAuthTypeEnum; +import cd.casic.module.terminal.host.config.model.HostSshConfigModel; +import cd.casic.module.terminal.utils.RsaParamDecryptUtils; +import cd.casic.module.terminal.utils.Valid; +import cn.orionsec.kit.lang.constant.Const; +import cn.orionsec.kit.lang.utils.Booleans; +import cn.orionsec.kit.lang.utils.Charsets; +import org.apache.logging.log4j.util.Strings; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * 主机 SSH 配置策略 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2023/9/19 14:26 + */ +@Component +public class HostSshConfigStrategy extends AbstractGenericsDataStrategy { + + @Autowired + private SecretKeyMapper secretKeyMapper; + + @Autowired + private MachineInfoMapper machineInfoMapper; + + private static final String USERNAME = "root"; + + public HostSshConfigStrategy() { + super(HostSshConfigModel.class); + } + + @Override + public HostSshConfigModel getDefault() { + return HostSshConfigModel.builder() + .port(22) + .username(USERNAME) + .authType(HostSshAuthTypeEnum.PASSWORD.name()) + .connectTimeout(Const.MS_S_10) + .charset(Const.UTF_8) + .fileNameCharset(Const.UTF_8) + .fileContentCharset(Const.UTF_8) + .build(); + } + + @Override + protected void preValid(HostSshConfigModel model) { + // 验证编码格式 + this.validCharset(model.getCharset()); + this.validCharset(model.getFileNameCharset()); + this.validCharset(model.getFileContentCharset()); + // 检查主机密钥是否存在 + Long keyId = model.getKeyId(); + if (keyId != null) { + Valid.notNull(secretKeyMapper.selectById(keyId), ErrorMessage.KEY_ABSENT); + } + // 检查主机身份是否存在 + Long identityId = model.getIdentityId(); + if (identityId != null) { + Valid.notNull(machineInfoMapper.selectById(identityId), ErrorMessage.IDENTITY_ABSENT); + } + } + + @Override + protected void valid(HostSshConfigModel model) { + // 验证填充后的参数 + Valid.valid(model); + } + + @Override + protected void updateFill(HostSshConfigModel beforeModel, HostSshConfigModel afterModel) { + // 加密密码 + this.checkEncryptPassword(beforeModel, afterModel); + afterModel.setHasPassword(null); + afterModel.setUseNewPassword(null); + } + + @Override + public void toView(HostSshConfigModel model) { + if (model == null) { + return; + } + model.setHasPassword(Strings.isNotBlank(model.getPassword())); + model.setPassword(null); + } + + /** + * 检查加密密码 + * + * @param before before + * @param after after + */ + private void checkEncryptPassword(HostSshConfigModel before, HostSshConfigModel after) { + // 非密码认证/使用原始密码则直接赋值 + if (!HostSshAuthTypeEnum.PASSWORD.name().equals(after.getAuthType()) + || !Booleans.isTrue(after.getUseNewPassword())) { + if (before != null) { + after.setPassword(before.getPassword()); + } + return; + } + // 检查新密码 + String newPassword = Valid.notBlank(after.getPassword(), ErrorMessage.PASSWORD_MISSING); + // 解密密码 + newPassword = RsaParamDecryptUtils.decrypt(newPassword); + Valid.notBlank(newPassword, ErrorMessage.DECRYPT_ERROR); + // 设置密码 + after.setPassword(AesEncryptUtils.encryptAsString(newPassword)); + } + + /** + * 检查编码格式 + * + * @param charset charset + */ + private void validCharset(String charset) { + Valid.isTrue(Charsets.isSupported(charset), ErrorMessage.UNSUPPORTED_CHARSET, charset); + } + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/extra/model/HostLabelExtraModel.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/extra/model/HostLabelExtraModel.java new file mode 100644 index 00000000..4d7ca584 --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/extra/model/HostLabelExtraModel.java @@ -0,0 +1,32 @@ +package cd.casic.module.terminal.host.extra.model; + +import cd.casic.module.terminal.common.GenericsDataModel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 主机拓展信息 - 标签模型 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2024/2/29 23:16 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class HostLabelExtraModel implements GenericsDataModel { + + /** + * 别名 + */ + private String alias; + + /** + * 颜色 + */ + private String color; + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/extra/model/HostSpecExtraModel.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/extra/model/HostSpecExtraModel.java new file mode 100644 index 00000000..a4e32680 --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/extra/model/HostSpecExtraModel.java @@ -0,0 +1,119 @@ +package cd.casic.module.terminal.host.extra.model; + +import cd.casic.module.terminal.common.GenericsDataModel; +import lombok.*; + +import java.util.Date; +import java.util.List; + +/** + * 主机拓展信息 - 规格模型 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2025/3/24 0:34 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class HostSpecExtraModel implements GenericsDataModel { + + /** + * sn + */ + private String sn; + + /** + * 系统名称 + */ + private String osName; + + /** + * cpu 核心数 + */ + private Integer cpuCore; + + /** + * cpu 频率 + */ + private Double cpuFrequency; + + /** + * cpu 型号 + */ + private String cpuModel; + + /** + * 内存大小 + */ + private Double memorySize; + + /** + * 硬盘大小 + */ + private Double diskSize; + + /** + * 上行带宽 + */ + private Integer inBandwidth; + + /** + * 下行带宽 + */ + private Integer outBandwidth; + + /** + * 公网 ip 列表 + */ + private List publicIpAddress; + + /** + * 内网 ip 列表 + */ + private List privateIpAddress; + + /** + * 负责人 + */ + private String chargePerson; + + /** + * 创建时间 + */ + private Date createdTime; + + /** + * 到期时间 + */ + private Date expiredTime; + + /** + * 扩展信息 + */ + @Singular + private List items; + + /** + * 扩展信息项 + */ + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class HostSpecExtraItem { + + /** + * 标签 + */ + private String label; + + /** + * 值 + */ + private String value; + + } + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/extra/model/HostSshExtraModel.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/extra/model/HostSshExtraModel.java new file mode 100644 index 00000000..117ba6a5 --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/extra/model/HostSshExtraModel.java @@ -0,0 +1,42 @@ +package cd.casic.module.terminal.host.extra.model; + +import cd.casic.module.terminal.common.GenericsDataModel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 主机拓展信息 - ssh 模型 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2023/12/20 21:36 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class HostSshExtraModel implements GenericsDataModel { + + /** + * 认证方式 + */ + private String authType; + + /** + * 认证方式 + */ + private String username; + + /** + * 主机密钥 + */ + private Long keyId; + + /** + * 主机身份 + */ + private Long identityId; + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/extra/strategy/HostLabelExtraStrategy.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/extra/strategy/HostLabelExtraStrategy.java new file mode 100644 index 00000000..6f173b22 --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/extra/strategy/HostLabelExtraStrategy.java @@ -0,0 +1,43 @@ +package cd.casic.module.terminal.host.extra.strategy; + +import cd.casic.module.terminal.common.AbstractGenericsDataStrategy; +import cd.casic.module.terminal.host.extra.model.HostLabelExtraModel; +import cn.orionsec.kit.lang.constant.Const; +import org.springframework.stereotype.Component; + +/** + * 主机拓展信息 - 标签模型处理策略 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2024/2/29 23:16 + */ +@Component +public class HostLabelExtraStrategy extends AbstractGenericsDataStrategy { + + public HostLabelExtraStrategy() { + super(HostLabelExtraModel.class); + } + + @Override + public HostLabelExtraModel getDefault() { + return HostLabelExtraModel.builder() + // 透明 + .color(Const.EMPTY) + // 无别名 + .alias(Const.EMPTY) + .build(); + } + + @Override + public void updateFill(HostLabelExtraModel beforeModel, HostLabelExtraModel afterModel) { + // 为空则覆盖 + if (afterModel.getAlias() == null) { + afterModel.setAlias(beforeModel.getAlias()); + } + if (afterModel.getColor() == null) { + afterModel.setColor(beforeModel.getColor()); + } + } + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/extra/strategy/HostSpecExtraStrategy.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/extra/strategy/HostSpecExtraStrategy.java new file mode 100644 index 00000000..aa4e8c42 --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/extra/strategy/HostSpecExtraStrategy.java @@ -0,0 +1,27 @@ +package cd.casic.module.terminal.host.extra.strategy; + + +import cd.casic.module.terminal.common.AbstractGenericsDataStrategy; +import cd.casic.module.terminal.host.extra.model.HostSpecExtraModel; +import org.springframework.stereotype.Component; + +/** + * 主机规格额外信息策略 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2025/3/24 0:21 + */ +@Component +public class HostSpecExtraStrategy extends AbstractGenericsDataStrategy { + + public HostSpecExtraStrategy() { + super(HostSpecExtraModel.class); + } + + @Override + public HostSpecExtraModel getDefault() { + return new HostSpecExtraModel(); + } + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/jsch/SessionMessage.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/jsch/SessionMessage.java new file mode 100644 index 00000000..9e0b6023 --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/jsch/SessionMessage.java @@ -0,0 +1,18 @@ +package cd.casic.module.terminal.host.jsch; + +/** + * 连接消息 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2024/7/11 16:30 + */ +public interface SessionMessage { + + String AUTHENTICATION_FAILURE = "身份认证失败. {}"; + + String SERVER_UNREACHABLE = "无法连接至服务器. {}"; + + String CONNECTION_TIMEOUT = "连接服务器超时. {}"; + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/jsch/SessionStores.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/jsch/SessionStores.java new file mode 100644 index 00000000..2f7c264e --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/jsch/SessionStores.java @@ -0,0 +1,134 @@ +package cd.casic.module.terminal.host.jsch; + + +import cd.casic.module.terminal.common.AesEncryptUtils; +import cd.casic.module.terminal.common.AppConst; +import cd.casic.module.terminal.controller.dto.TerminalConnectDTO; +import cn.orionsec.kit.lang.constant.Const; +import cn.orionsec.kit.lang.exception.AuthenticationException; +import cn.orionsec.kit.lang.exception.argument.InvalidArgumentException; +import cn.orionsec.kit.lang.utils.Exceptions; +import cn.orionsec.kit.lang.utils.Strings; +import cn.orionsec.kit.net.host.SessionHolder; +import cn.orionsec.kit.net.host.SessionStore; +import lombok.extern.slf4j.Slf4j; + +import java.util.Optional; + +/** + * sessionStore 工具类 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2024/7/11 16:58 + */ +@Slf4j +public class SessionStores { + + protected static final ThreadLocal CURRENT_ADDRESS = new ThreadLocal<>(); + + /** + * 打开 sessionStore + * + * @param conn conn + * @return sessionStore + */ + public static SessionStore openSessionStore(TerminalConnectDTO conn) { + Long hostId = conn.getHostId(); + String address = conn.getHostAddress(); + String username = conn.getUsername(); + log.info("SessionStores-open-start hostId: {}, address: {}, username: {}", hostId, address, username); + try { + CURRENT_ADDRESS.set(address); + // 创建会话 + SessionHolder sessionHolder = SessionHolder.create(); +// sessionHolder.setLogger(SessionLogger.INFO); + SessionStore session = createSessionStore(conn, sessionHolder); + // 设置版本 + session.getSession().setClientVersion("SSH-2.0-ORION_VISOR_V" + AppConst.VERSION); + // 连接 + session.connect(); + log.info("SessionStores-open-success hostId: {}, address: {}, username: {}", hostId, address, username); + return session; + } catch (Exception e) { + String message = e.getMessage(); + log.error("SessionStores-open-error hostId: {}, address: {}, username: {}, message: {}", hostId, address, username, message, e); + throw Exceptions.app(getErrorMessage(e), e); + } finally { + CURRENT_ADDRESS.remove(); + } + } + + /** + * 创建 sessionStore + * + * @param conn conn + * @param sessionHolder sessionHolder + * @return sessionStore + */ + private static SessionStore createSessionStore(TerminalConnectDTO conn, SessionHolder sessionHolder) { + final boolean useKey = conn.getKeyId() != null; + // 使用密钥认证 + if (useKey) { + // 加载密钥 + String publicKey = Optional.ofNullable(conn.getPublicKey()) + .map(AesEncryptUtils::decryptAsString) + .orElse(null); + String privateKey = Optional.ofNullable(conn.getPrivateKey()) + .map(AesEncryptUtils::decryptAsString) + .orElse(null); + String password = Optional.ofNullable(conn.getPrivateKeyPassword()) + .map(AesEncryptUtils::decryptAsString) + .orElse(null); + sessionHolder.addIdentityValue(String.valueOf(conn.getKeyId()), + privateKey, + publicKey, + password); + } + // 获取会话 + SessionStore session = sessionHolder.getSession(conn.getHostAddress(), conn.getHostPort(), conn.getUsername()); + // 使用密码认证 + if (!useKey) { + String password = conn.getPassword(); + if (!Strings.isEmpty(password)) { +// session.password(AesEncryptUtils.decryptAsString(password)); + session.password(password); + } + } + // 超时时间 + session.timeout(conn.getTimeout()); + return session; + } + + /** + * 获取错误信息 + * + * @param e e + * @return errorMessage + */ + private static String getErrorMessage(Exception e) { + if (e == null) { + return null; + } + String host = CURRENT_ADDRESS.get(); + String message = e.getMessage(); + if (Strings.contains(message, Const.TIMEOUT)) { + // 连接超时 + return Strings.format(SessionMessage.CONNECTION_TIMEOUT, host); + } else if (Exceptions.isCausedBy(e, AuthenticationException.class)) { + // 认证失败 + return Strings.format(SessionMessage.AUTHENTICATION_FAILURE, host); + } else if (Exceptions.isCausedBy(e, InvalidArgumentException.class)) { + // 参数错误 + if (Strings.isBlank(message)) { + return Strings.format(SessionMessage.SERVER_UNREACHABLE, host); + } else { + return message; + } + } else { + // 其他错误 + return Strings.format(SessionMessage.SERVER_UNREACHABLE, host); + } + } + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/constant/TerminalMessage.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/constant/TerminalMessage.java new file mode 100644 index 00000000..1e60a0ee --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/constant/TerminalMessage.java @@ -0,0 +1,26 @@ +package cd.casic.module.terminal.host.terminal.constant; + +/** + * 终端信息常量 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2024/1/3 16:57 + */ +public interface TerminalMessage { + + String CONFIG_DISABLED = "SSH configuration has been disabled."; + + String AUTHENTICATION_FAILURE = "authentication failed. please check the configuration."; + + String SERVER_UNREACHABLE = "remote server unreachable. please check the configuration."; + + String CONNECTION_FAILED = "connection failed."; + + String CONNECTION_TIMEOUT = "connection timeout."; + + String CONNECTION_CLOSED = "connection closed."; + + String FORCED_OFFLINE = "forced offline."; + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/dto/SftpGetContentCacheDTO.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/dto/SftpGetContentCacheDTO.java new file mode 100644 index 00000000..93edacdb --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/dto/SftpGetContentCacheDTO.java @@ -0,0 +1,33 @@ +package cd.casic.module.terminal.host.terminal.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; + +/** + * sftp 获取文件内容缓存对象 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2024/10/10 20:49 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Schema(name = "SftpGetContentCacheDTO", description = "sftp 获取文件内容缓存对象") +public class SftpGetContentCacheDTO implements Serializable { + + private static final long serialVersionUID = 1L; + + @Schema(description = "主机id") + private Long hostId; + + @Schema(description = "文件路径") + private String path; + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/dto/SftpSetContentCacheDTO.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/dto/SftpSetContentCacheDTO.java new file mode 100644 index 00000000..fb60f09f --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/dto/SftpSetContentCacheDTO.java @@ -0,0 +1,33 @@ +package cd.casic.module.terminal.host.terminal.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; + +/** + * sftp 获取文件内容缓存对象 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2024/10/10 20:49 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Schema(name = "SftpSetContentCacheDTO", description = "sftp 设置文件内容缓存对象") +public class SftpSetContentCacheDTO implements Serializable { + + private static final long serialVersionUID = 1L; + + @Schema(description = "主机id") + private Long hostId; + + @Schema(description = "文件路径") + private String path; + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/enums/InputTypeEnum.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/enums/InputTypeEnum.java new file mode 100644 index 00000000..c9f7350a --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/enums/InputTypeEnum.java @@ -0,0 +1,260 @@ +package cd.casic.module.terminal.host.terminal.enums; + +import cd.casic.module.terminal.common.holder.SpringHolder; +import cd.casic.module.terminal.host.terminal.handler.*; +import cd.casic.module.terminal.host.terminal.model.TerminalBasePayload; +import cd.casic.module.terminal.host.terminal.model.request.*; +import com.alibaba.fastjson.JSONObject; +import jakarta.annotation.PostConstruct; +import lombok.Getter; +import org.springframework.stereotype.Component; + + +/** + * 输入操作类型枚举 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2023/12/29 15:33 + */ +public enum InputTypeEnum { + + /** + * 终端连接检查 + */ + CHECK("ck", + TerminalCheckHandler.class, + new String[]{"type", "sessionId", "hostId", "connectType"}, + TerminalCheckRequest.class, + true), + + + /** + * 连接主机 + */ + CONNECT("co", + TerminalConnectHandler.class, + new String[]{"type", "sessionId", "terminalType", "cols", "rows"}, + TerminalConnectRequest.class, + true), + + /** + * 关闭连接 + */ + CLOSE("cl", + TerminalCloseHandler.class, + new String[]{"type", "sessionId"}, + TerminalBasePayload.class, + true), + + /** + * ping + */ + PING("p", + TerminalPingHandler.class, + new String[]{"type"}, + TerminalBasePayload.class, + true), + + /** + * SSH 修改大小 + */ + SSH_RESIZE("rs", + SshResizeHandler.class, + new String[]{"type", "sessionId", "cols", "rows"}, + SshResizeRequest.class, + true), + + /** + * SSH 输入 + */ + SSH_INPUT("i", + SshInputHandler.class, + new String[]{"type", "sessionId", "command"}, + SshInputRequest.class, + false), + + /** + * SFTP 文件列表 + */ + SFTP_LIST("ls", + SftpListHandler.class, + new String[]{"type", "sessionId", "showHiddenFile", "path"}, + SftpListRequest.class, + true), + + /** + * SFTP 创建文件夹 + */ + SFTP_MKDIR("mk", + SftpMakeDirectoryHandler.class, + new String[]{"type", "sessionId", "path"}, + SftpBaseRequest.class, + true), + + /** + * SFTP 创建文件 + */ + SFTP_TOUCH("to", + SftpTouchHandler.class, + new String[]{"type", "sessionId", "path"}, + SftpBaseRequest.class, + true), + + /** + * SFTP 移动文件 + */ + SFTP_MOVE("mv", + SftpMoveHandler.class, + new String[]{"type", "sessionId", "path", "target"}, + SftpMoveRequest.class, + true), + + /** + * SFTP 删除文件 + */ + SFTP_REMOVE("rm", + SftpRemoveHandler.class, + new String[]{"type", "sessionId", "path"}, + SftpBaseRequest.class, + true), + + /** + * SFTP 截断文件 + */ + SFTP_TRUNCATE("tc", + SftpTruncateHandler.class, + new String[]{"type", "sessionId", "path"}, + SftpBaseRequest.class, + true), + + /** + * SFTP 修改文件权限 + */ + SFTP_CHMOD("cm", + SftpChangeModeHandler.class, + new String[]{"type", "sessionId", "path", "mod"}, + SftpChangeModeRequest.class, + true), + + /** + * SFTP 下载文件夹展开文件 + */ + SFTP_DOWNLOAD_FLAT_DIRECTORY("df", + SftpDownloadFlatDirectoryHandler.class, + new String[]{"type", "sessionId", "currentPath", "path"}, + SftpDownloadFlatDirectoryRequest.class, + true), + + /** + * SFTP 获取内容 + */ + SFTP_GET_CONTENT("gc", + SftpGetContentHandler.class, + new String[]{"type", "sessionId", "path"}, + SftpBaseRequest.class, + true), + + /** + * SFTP 修改内容 + */ + SFTP_SET_CONTENT("sc", + SftpSetContentHandler.class, + new String[]{"type", "sessionId", "path"}, + SftpBaseRequest.class, + true), + ; + + private static final char SEPARATOR = '|'; + + @Getter + private final String type; + + private final Class> handlerBean; + + private final String[] payloadDefine; + + private final Class payloadClass; + + @Getter + private final boolean asyncExec; + + @Getter + private ITerminalHandler handler; + + + InputTypeEnum(String type, + Class> handlerBean, + String[] payloadDefine, + Class payloadClass, + boolean asyncExec) { + this.type = type; + this.handlerBean = handlerBean; + this.payloadDefine = payloadDefine; + this.payloadClass = payloadClass; + this.asyncExec = asyncExec; + } + + public static InputTypeEnum of(String payload) { + if (payload == null) { + return null; + } + for (InputTypeEnum value : values()) { + if (payload.startsWith(value.type + SEPARATOR) || payload.equals(value.type)) { + return value; + } + } + return null; + } + + /** + * 解析请求 + * + * @param payload payload + * @param T + * @return payload + */ + @SuppressWarnings("unchecked") + public T parse(String payload) { + JSONObject object = new JSONObject(); + int curr = 0; + int len = payload.length(); + for (int i = 0, pl = payloadDefine.length; i < pl; i++) { + if (i == pl - 1) { + // 最后一次 + object.put(payloadDefine[i], payload.substring(curr, len)); + } else { + // 非最后一次 + StringBuilder tmp = new StringBuilder(); + for (; curr < len; curr++) { + char c = payload.charAt(curr); + if (c == SEPARATOR) { + object.put(payloadDefine[i], tmp.toString()); + curr++; + break; + } else { + tmp.append(c); + } + } + } + } + return (T) object.toJavaObject(payloadClass); + } + + /** + * 类型字段定义 + */ + @Component + static class TypeFieldDefinition { + + @PostConstruct + public void init() { + for (InputTypeEnum value : InputTypeEnum.values()) { + value.handler = SpringHolder.getBean(value.handlerBean); + } + + } + + } + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/enums/OutputTypeEnum.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/enums/OutputTypeEnum.java new file mode 100644 index 00000000..82201e12 --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/enums/OutputTypeEnum.java @@ -0,0 +1,121 @@ +package cd.casic.module.terminal.host.terminal.enums; + +import cn.orionsec.kit.lang.utils.json.matcher.ReplacementFormatters; +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 输出操作类型枚举 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2023/12/29 15:33 + */ +@Getter +@AllArgsConstructor +public enum OutputTypeEnum { + + /** + * 终端连接检查 + */ + CHECK("ck", "${type}|${sessionId}|${result}|${msg}"), + + /** + * 终端连接 + */ + CONNECT("co", "${type}|${sessionId}|${result}|${msg}"), + + /** + * 关闭连接 + */ + CLOSE("cl", "${type}|${sessionId}|${forceClose}|${msg}"), + + /** + * pong + */ + PONG("p", "${type}"), + + /** + * SSH 输出 + */ + SSH_OUTPUT("o", "${type}|${sessionId}|${body}"), + + /** + * SFTP 文件列表 + */ + SFTP_LIST("ls", "${type}|${sessionId}|${path}|${result}|${msg}|${body}"), + + /** + * SFTP 创建文件夹 + */ + SFTP_MKDIR("mk", "${type}|${sessionId}|${result}|${msg}"), + + /** + * SFTP 创建文件 + */ + SFTP_TOUCH("to", "${type}|${sessionId}|${result}|${msg}"), + + /** + * SFTP 移动文件 + */ + SFTP_MOVE("mv", "${type}|${sessionId}|${result}|${msg}"), + + /** + * SFTP 删除文件 + */ + SFTP_REMOVE("rm", "${type}|${sessionId}|${result}|${msg}"), + + /** + * SFTP 截断文件 + */ + SFTP_TRUNCATE("tc", "${type}|${sessionId}|${result}|${msg}"), + + /** + * SFTP 修改文件权限 + */ + SFTP_CHMOD("cm", "${type}|${sessionId}|${result}|${msg}"), + + /** + * SFTP 下载文件夹展开文件 + */ + SFTP_DOWNLOAD_FLAT_DIRECTORY("df", "${type}|${sessionId}|${currentPath}|${result}|${msg}|${body}"), + + /** + * SFTP 获取文件内容 + */ + SFTP_GET_CONTENT("gc", "${type}|${sessionId}|${result}|${msg}|${token}"), + + /** + * SFTP 修改文件内容 + */ + SFTP_SET_CONTENT("sc", "${type}|${sessionId}|${result}|${msg}|${token}"), + + ; + + private final String type; + + private final String template; + + /** + * 格式化 + * + * @param o o + * @return 格式化 + */ + public String format(Object o) { + return ReplacementFormatters.format(this.template, o); + } + + public static OutputTypeEnum of(String type) { + if (type == null) { + return null; + } + for (OutputTypeEnum value : values()) { + if (value.type.equals(type)) { + return value; + } + } + return null; + } + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/enums/TerminalConnectStatusEnum.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/enums/TerminalConnectStatusEnum.java new file mode 100644 index 00000000..e90ec04a --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/enums/TerminalConnectStatusEnum.java @@ -0,0 +1,51 @@ +package cd.casic.module.terminal.host.terminal.enums; + +import cn.orionsec.kit.lang.utils.collect.Lists; +import java.util.List; + +/** + * 终端连接状态 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2023/12/26 22:27 + */ +public enum TerminalConnectStatusEnum { + + /** + * 连接中 + */ + CONNECTING, + + /** + * 完成 + */ + COMPLETE, + + /** + * 失败 + */ + FAILED, + + /** + * 强制下线 + */ + FORCE_OFFLINE, + + ; + + public static final List FINISH_STATUS_LIST = Lists.of(COMPLETE.name(), FAILED.name(), FORCE_OFFLINE.name()); + + public static TerminalConnectStatusEnum of(String type) { + if (type == null) { + return null; + } + for (TerminalConnectStatusEnum value : values()) { + if (value.name().equals(type)) { + return value; + } + } + return null; + } + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/handler/AbstractTerminalHandler.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/handler/AbstractTerminalHandler.java new file mode 100644 index 00000000..f7e15660 --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/handler/AbstractTerminalHandler.java @@ -0,0 +1,98 @@ +package cd.casic.module.terminal.host.terminal.handler; + + +import cd.casic.module.terminal.common.ErrorMessage; +import cd.casic.module.terminal.common.OperatorLogs; +import cd.casic.module.terminal.common.WebSockets; +import cd.casic.module.terminal.host.terminal.enums.OutputTypeEnum; +import cd.casic.module.terminal.host.terminal.manager.TerminalManager; +import cd.casic.module.terminal.host.terminal.model.TerminalBasePayload; +import cd.casic.module.terminal.host.terminal.model.TerminalConfig; +import cd.casic.module.terminal.host.terminal.session.ITerminalSession; +import jakarta.annotation.Resource; +import org.springframework.web.socket.WebSocketSession; +import java.util.Map; + +/** + * 终端消息处理器 基类 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2023/12/29 18:59 + */ +public abstract class AbstractTerminalHandler implements ITerminalHandler { + + @Resource + protected TerminalManager terminalManager; + + + /** + * 发送消息 + * + * @param channel channel + * @param type type + * @param body body + * @param E + */ + public void send(WebSocketSession channel, OutputTypeEnum type, E body) { + body.setType(type.getType()); + // 发送消息 + this.send(channel, type.format(body)); + } + + /** + * 发送消息 + * + * @param channel channel + * @param message message + */ + protected void send(WebSocketSession channel, String message) { + WebSockets.sendText(channel, message); + } + + /** + * 保存操作日志 + * + * @param payload payload + * @param channel channel + * @param extra extra + * @param type type + * @param startTime startTime + * @param ex ex + */ + protected void saveOperatorLog(T payload, + WebSocketSession channel, + Map extra, + String type, + long startTime, + Exception ex) { + String channelId = channel.getId(); + String sessionId = payload.getSessionId(); + // 获取会话并且设置参数 + ITerminalSession session = terminalManager.getSession(channelId, sessionId); + if (session != null) { + TerminalConfig config = session.getConfig(); + extra.put(OperatorLogs.HOST_ID, config.getHostId()); + extra.put(OperatorLogs.HOST_NAME, config.getHostName()); + extra.put(OperatorLogs.ADDRESS, config.getAddress()); + } + extra.put(OperatorLogs.CHANNEL_ID, channelId); + extra.put(OperatorLogs.SESSION_ID, sessionId); + // 获取日志 +// OperatorLogModel model = TerminalUtils.getOperatorLogModel(channel, extra, type, startTime, ex); + // 保存 +// operatorLogFrameworkService.insert(model); + } + + /** + * 获取错误信息 + * + * @param ex ex + * @return msg + */ + protected String getErrorMessage(Exception ex) { + // 获取错误信息 + return ErrorMessage.getErrorMessage(ex, ErrorMessage.OPERATE_ERROR); + } + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/handler/ITerminalHandler.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/handler/ITerminalHandler.java new file mode 100644 index 00000000..6584bb7f --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/handler/ITerminalHandler.java @@ -0,0 +1,23 @@ +package cd.casic.module.terminal.host.terminal.handler; + +import cd.casic.module.terminal.host.terminal.model.TerminalBasePayload; +import org.springframework.web.socket.WebSocketSession; + +/** + * 终端消息处理器 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2023/12/29 18:53 + */ +public interface ITerminalHandler { + + /** + * 处理消息 + * + * @param channel channel + * @param payload payload + */ + void handle(WebSocketSession channel, T payload); + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/handler/SftpChangeModeHandler.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/handler/SftpChangeModeHandler.java new file mode 100644 index 00000000..ba74b14b --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/handler/SftpChangeModeHandler.java @@ -0,0 +1,65 @@ +package cd.casic.module.terminal.host.terminal.handler; + +import cd.casic.module.terminal.common.BooleanBit; +import cd.casic.module.terminal.common.OperatorLogs; +import cd.casic.module.terminal.define.operator.TerminalOperatorType; +import cd.casic.module.terminal.host.terminal.enums.OutputTypeEnum; +import cd.casic.module.terminal.host.terminal.model.request.SftpChangeModeRequest; +import cd.casic.module.terminal.host.terminal.model.response.SftpBaseResponse; +import cd.casic.module.terminal.host.terminal.session.ISftpSession; + +import cn.orionsec.kit.lang.utils.collect.Maps; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.web.socket.WebSocketSession; + +import java.util.Map; + + +/** + * sftp 修改文件权限 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2024/2/19 11:13 + */ +@Slf4j +@Component +public class SftpChangeModeHandler extends AbstractTerminalHandler { + + @Override + public void handle(WebSocketSession channel, SftpChangeModeRequest payload) { + long startTime = System.currentTimeMillis(); + // 获取会话 + String sessionId = payload.getSessionId(); + ISftpSession session = terminalManager.getSession(channel.getId(), sessionId); + String path = payload.getPath(); + Integer mod = payload.getMod(); + log.info("SftpChangeModeHandler-handle start sessionId: {}, path: {}, mod: {}", sessionId, path, mod); + Exception ex = null; + // 修改权限 + try { + session.chmod(path, mod); + log.info("SftpChangeModeHandler-handle success sessionId: {}, path: {}, mod: {}", sessionId, path, mod); + } catch (Exception e) { + log.error("SftpChangeModeHandler-handle error sessionId: {}", sessionId, e); + ex = e; + } + // 返回 + this.send(channel, + OutputTypeEnum.SFTP_CHMOD, + SftpBaseResponse.builder() + .sessionId(sessionId) + .result(BooleanBit.of(ex == null).getValue()) + .msg(this.getErrorMessage(ex)) + .build()); + // 保存操作日志 + Map extra = Maps.newMap(); + extra.put(OperatorLogs.PATH, path); + extra.put(OperatorLogs.MOD, mod); + this.saveOperatorLog(payload, channel, + extra, TerminalOperatorType.SFTP_CHMOD, + startTime, ex); + } + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/handler/SftpDownloadFlatDirectoryHandler.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/handler/SftpDownloadFlatDirectoryHandler.java new file mode 100644 index 00000000..58aaadea --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/handler/SftpDownloadFlatDirectoryHandler.java @@ -0,0 +1,58 @@ +package cd.casic.module.terminal.host.terminal.handler; + +import cd.casic.module.terminal.common.BooleanBit; +import cd.casic.module.terminal.host.terminal.enums.OutputTypeEnum; +import cd.casic.module.terminal.host.terminal.model.request.SftpDownloadFlatDirectoryRequest; +import cd.casic.module.terminal.host.terminal.model.response.SftpDownloadFlatDirectoryResponse; +import cd.casic.module.terminal.host.terminal.model.response.SftpFileVO; +import cd.casic.module.terminal.host.terminal.session.ISftpSession; +import cn.orionsec.kit.lang.utils.collect.Lists; +import com.alibaba.fastjson.JSON; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.web.socket.WebSocketSession; +import java.util.Arrays; +import java.util.List; + +/** + * sftp 下载文件夹展开文件 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2024/2/19 11:13 + */ +@Slf4j +@Component +public class SftpDownloadFlatDirectoryHandler extends AbstractTerminalHandler { + + @Override + public void handle(WebSocketSession channel, SftpDownloadFlatDirectoryRequest payload) { + // 获取会话 + String sessionId = payload.getSessionId(); + ISftpSession session = terminalManager.getSession(channel.getId(), sessionId); + String[] paths = payload.getPath().split("\\|"); + log.info("SftpDownloadFlatDirectoryHandler-handle start sessionId: {}, paths: {}", sessionId, Arrays.toString(paths)); + Exception ex = null; + List files = Lists.empty(); + // 展开文件夹内的全部文件 + try { + files = session.flatDirectory(paths); + log.info("SftpDownloadFlatDirectoryHandler-handle success sessionId: {}, paths: {}", sessionId, Arrays.toString(paths)); + } catch (Exception e) { + log.error("SftpDownloadFlatDirectoryHandler-handle error sessionId: {}", sessionId, e); + ex = e; + } + // 返回 + this.send(channel, + OutputTypeEnum.SFTP_DOWNLOAD_FLAT_DIRECTORY, + SftpDownloadFlatDirectoryResponse.builder() + .sessionId(sessionId) + .currentPath(payload.getCurrentPath()) + .body(JSON.toJSONString(files)) + .result(BooleanBit.of(ex == null).getValue()) + .msg(this.getErrorMessage(ex)) + .build()); + } + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/handler/SftpGetContentHandler.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/handler/SftpGetContentHandler.java new file mode 100644 index 00000000..2134ca6f --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/handler/SftpGetContentHandler.java @@ -0,0 +1,66 @@ +package cd.casic.module.terminal.host.terminal.handler; + +import cd.casic.module.terminal.common.BooleanBit; +import cd.casic.module.terminal.dal.redis.TerminalRedisDAO; +import cd.casic.module.terminal.define.cache.TerminalCacheKeyDefine; +import cd.casic.module.terminal.host.terminal.dto.SftpGetContentCacheDTO; +import cd.casic.module.terminal.host.terminal.enums.OutputTypeEnum; +import cd.casic.module.terminal.host.terminal.model.request.SftpBaseRequest; +import cd.casic.module.terminal.host.terminal.model.response.SftpGetContentResponse; +import cd.casic.module.terminal.host.terminal.session.ISftpSession; +import cn.orionsec.kit.lang.id.UUIds; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.web.socket.WebSocketSession; + +/** + * sftp 获取文件内容 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2024/2/19 11:13 + */ +@Slf4j +@Component +public class SftpGetContentHandler extends AbstractTerminalHandler { + + @Resource + private TerminalRedisDAO terminalRedisDAO; + + @Override + public void handle(WebSocketSession channel, SftpBaseRequest payload) { + // 获取会话 + String sessionId = payload.getSessionId(); + ISftpSession session = terminalManager.getSession(channel.getId(), sessionId); + String path = payload.getPath(); + log.info("SftpGetContentHandler-handle start sessionId: {}, path: {}", sessionId, path); + String token = UUIds.random32(); + Exception ex = null; + try { + // 检查文件是否可编辑 + session.checkCanEdit(path); + // 设置缓存 + String key = TerminalCacheKeyDefine.TERMINAL_SFTP_GET_CONTENT.format(token); + SftpGetContentCacheDTO cache = SftpGetContentCacheDTO.builder() + .hostId(session.getConfig().getHostId()) + .path(path) + .build(); + terminalRedisDAO.setJson(key, TerminalCacheKeyDefine.TERMINAL_SFTP_GET_CONTENT, cache); + log.info("SftpGetContentHandler-handle success sessionId: {}, path: {}", sessionId, path); + } catch (Exception e) { + log.error("SftpGetContentHandler-handle error sessionId: {}", sessionId, e); + ex = e; + } + // 返回 + this.send(channel, + OutputTypeEnum.SFTP_GET_CONTENT, + SftpGetContentResponse.builder() + .sessionId(sessionId) + .result(BooleanBit.of(ex == null).getValue()) + .token(token) + .msg(this.getErrorMessage(ex)) + .build()); + } + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/handler/SftpListHandler.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/handler/SftpListHandler.java new file mode 100644 index 00000000..1a985447 --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/handler/SftpListHandler.java @@ -0,0 +1,62 @@ +package cd.casic.module.terminal.host.terminal.handler; + +import cd.casic.module.terminal.common.BooleanBit; +import cd.casic.module.terminal.host.terminal.enums.OutputTypeEnum; +import cd.casic.module.terminal.host.terminal.model.request.SftpListRequest; +import cd.casic.module.terminal.host.terminal.model.response.SftpFileVO; +import cd.casic.module.terminal.host.terminal.model.response.SftpListResponse; +import cd.casic.module.terminal.host.terminal.session.ISftpSession; +import cn.orionsec.kit.lang.utils.collect.Lists; +import com.alibaba.fastjson.JSON; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.web.socket.WebSocketSession; +import java.util.List; + +/** + * sftp 文件列表 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2023/12/29 15:32 + */ +@Slf4j +@Component +public class SftpListHandler extends AbstractTerminalHandler { + + private static final String HOME_PATH = "~"; + + @Override + public void handle(WebSocketSession channel, SftpListRequest payload) { + // 获取会话 + String sessionId = payload.getSessionId(); + ISftpSession session = terminalManager.getSession(channel.getId(), sessionId); + String path = payload.getPath(); + log.info("SftpListHandler-handle start sessionId: {}, path: {}", sessionId, path); + Exception ex = null; + List list = Lists.empty(); + try { + // 空目录则直接获取 home 目录 + if (HOME_PATH.equals(path)) { + path = session.getHome(); + } + // 文件列表 + list = session.list(path, BooleanBit.toBoolean(payload.getShowHiddenFile())); + log.info("SftpListHandler-handle success sessionId: {}, path: {}", sessionId, path); + } catch (Exception e) { + log.error("SftpListHandler-handle error sessionId: {}", sessionId, e); + ex = e; + } + // 返回 + this.send(channel, + OutputTypeEnum.SFTP_LIST, + SftpListResponse.builder() + .sessionId(sessionId) + .result(BooleanBit.of(ex == null).getValue()) + .path(path) + .body(JSON.toJSONString(list)) + .build()); + } + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/handler/SftpMakeDirectoryHandler.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/handler/SftpMakeDirectoryHandler.java new file mode 100644 index 00000000..903de59e --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/handler/SftpMakeDirectoryHandler.java @@ -0,0 +1,60 @@ +package cd.casic.module.terminal.host.terminal.handler; + +import cd.casic.module.terminal.common.BooleanBit; +import cd.casic.module.terminal.common.OperatorLogs; +import cd.casic.module.terminal.define.operator.TerminalOperatorType; +import cd.casic.module.terminal.host.terminal.enums.OutputTypeEnum; +import cd.casic.module.terminal.host.terminal.model.request.SftpBaseRequest; +import cd.casic.module.terminal.host.terminal.model.response.SftpBaseResponse; +import cd.casic.module.terminal.host.terminal.session.ISftpSession; +import cn.orionsec.kit.lang.utils.collect.Maps; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.web.socket.WebSocketSession; +import java.util.Map; + +/** + * sftp 创建文件夹 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2024/2/19 11:13 + */ +@Slf4j +@Component +public class SftpMakeDirectoryHandler extends AbstractTerminalHandler { + + @Override + public void handle(WebSocketSession channel, SftpBaseRequest payload) { + long startTime = System.currentTimeMillis(); + // 获取会话 + String sessionId = payload.getSessionId(); + ISftpSession session = terminalManager.getSession(channel.getId(), sessionId); + String path = payload.getPath(); + log.info("SftpMakeDirectoryHandler-handle start sessionId: {}, path: {}", sessionId, path); + Exception ex = null; + // 创建文件夹 + try { + session.mkdir(path); + log.info("SftpMakeDirectoryHandler-handle success sessionId: {}, path: {}", sessionId, path); + } catch (Exception e) { + log.error("SftpMakeDirectoryHandler-handle error sessionId: {}", sessionId, e); + ex = e; + } + // 返回 + this.send(channel, + OutputTypeEnum.SFTP_MKDIR, + SftpBaseResponse.builder() + .sessionId(sessionId) + .result(BooleanBit.of(ex == null).getValue()) + .msg(this.getErrorMessage(ex)) + .build()); + // 保存操作日志 + Map extra = Maps.newMap(); + extra.put(OperatorLogs.PATH, path); + this.saveOperatorLog(payload, channel, + extra, TerminalOperatorType.SFTP_MKDIR, + startTime, ex); + } + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/handler/SftpMoveHandler.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/handler/SftpMoveHandler.java new file mode 100644 index 00000000..3e92802f --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/handler/SftpMoveHandler.java @@ -0,0 +1,62 @@ +package cd.casic.module.terminal.host.terminal.handler; + +import cd.casic.module.terminal.common.BooleanBit; +import cd.casic.module.terminal.common.OperatorLogs; +import cd.casic.module.terminal.define.operator.TerminalOperatorType; +import cd.casic.module.terminal.host.terminal.enums.OutputTypeEnum; +import cd.casic.module.terminal.host.terminal.model.request.SftpMoveRequest; +import cd.casic.module.terminal.host.terminal.model.response.SftpBaseResponse; +import cd.casic.module.terminal.host.terminal.session.ISftpSession; +import cn.orionsec.kit.lang.utils.collect.Maps; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.web.socket.WebSocketSession; +import java.util.Map; + +/** + * sftp 移动文件 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2024/2/19 11:13 + */ +@Slf4j +@Component +public class SftpMoveHandler extends AbstractTerminalHandler { + + @Override + public void handle(WebSocketSession channel, SftpMoveRequest payload) { + long startTime = System.currentTimeMillis(); + // 获取会话 + String sessionId = payload.getSessionId(); + ISftpSession session = terminalManager.getSession(channel.getId(), sessionId); + String path = payload.getPath(); + String target = payload.getTarget(); + log.info("SftpMoveHandler-handle start sessionId: {}, path: {}, target: {}", sessionId, path, target); + Exception ex = null; + // 移动 + try { + session.move(path, target); + log.info("SftpMoveHandler-handle success sessionId: {}, path: {}, target: {}", sessionId, path, target); + } catch (Exception e) { + log.error("SftpMoveHandler-handle error sessionId: {}", sessionId, e); + ex = e; + } + // 返回 + this.send(channel, + OutputTypeEnum.SFTP_MOVE, + SftpBaseResponse.builder() + .sessionId(sessionId) + .result(BooleanBit.of(ex == null).getValue()) + .msg(this.getErrorMessage(ex)) + .build()); + // 保存操作日志 + Map extra = Maps.newMap(); + extra.put(OperatorLogs.PATH, path); + extra.put(OperatorLogs.TARGET, target); + this.saveOperatorLog(payload, channel, + extra, TerminalOperatorType.SFTP_MOVE, + startTime, ex); + } + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/handler/SftpRemoveHandler.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/handler/SftpRemoveHandler.java new file mode 100644 index 00000000..fdb8ea55 --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/handler/SftpRemoveHandler.java @@ -0,0 +1,62 @@ +package cd.casic.module.terminal.host.terminal.handler; + +import cd.casic.module.terminal.common.BooleanBit; +import cd.casic.module.terminal.common.OperatorLogs; +import cd.casic.module.terminal.define.operator.TerminalOperatorType; +import cd.casic.module.terminal.host.terminal.enums.OutputTypeEnum; +import cd.casic.module.terminal.host.terminal.model.request.SftpBaseRequest; +import cd.casic.module.terminal.host.terminal.model.response.SftpBaseResponse; +import cd.casic.module.terminal.host.terminal.session.ISftpSession; +import cn.orionsec.kit.lang.utils.collect.Maps; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.web.socket.WebSocketSession; +import java.util.Arrays; +import java.util.Map; + +/** + * sftp 删除文件 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2024/2/19 11:13 + */ +@Slf4j +@Component +public class SftpRemoveHandler extends AbstractTerminalHandler { + + @Override + public void handle(WebSocketSession channel, SftpBaseRequest payload) { + long startTime = System.currentTimeMillis(); + String path = payload.getPath(); + String sessionId = payload.getSessionId(); + // 获取会话 + ISftpSession session = terminalManager.getSession(channel.getId(), sessionId); + String[] paths = path.split("\\|"); + log.info("SftpRemoveHandler-handle start sessionId: {}, path: {}", sessionId, Arrays.toString(paths)); + Exception ex = null; + // 删除 + try { + session.remove(paths); + log.info("SftpRemoveHandler-handle success sessionId: {}, path: {}", sessionId, Arrays.toString(paths)); + } catch (Exception e) { + log.error("SftpRemoveHandler-handle error sessionId: {}", sessionId, e); + ex = e; + } + // 返回 + this.send(channel, + OutputTypeEnum.SFTP_REMOVE, + SftpBaseResponse.builder() + .sessionId(sessionId) + .result(BooleanBit.of(ex == null).getValue()) + .msg(this.getErrorMessage(ex)) + .build()); + // 保存操作日志 + Map extra = Maps.newMap(); + extra.put(OperatorLogs.PATH, path); + this.saveOperatorLog(payload, channel, + extra, TerminalOperatorType.SFTP_REMOVE, + startTime, ex); + } + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/handler/SftpSetContentHandler.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/handler/SftpSetContentHandler.java new file mode 100644 index 00000000..07961eae --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/handler/SftpSetContentHandler.java @@ -0,0 +1,78 @@ +package cd.casic.module.terminal.host.terminal.handler; + +import cd.casic.module.terminal.common.BooleanBit; +import cd.casic.module.terminal.common.OperatorLogs; +import cd.casic.module.terminal.dal.redis.TerminalRedisDAO; +import cd.casic.module.terminal.define.cache.TerminalCacheKeyDefine; +import cd.casic.module.terminal.define.operator.TerminalOperatorType; +import cd.casic.module.terminal.host.terminal.dto.SftpSetContentCacheDTO; +import cd.casic.module.terminal.host.terminal.enums.OutputTypeEnum; +import cd.casic.module.terminal.host.terminal.model.request.SftpBaseRequest; +import cd.casic.module.terminal.host.terminal.model.response.SftpSetContentResponse; +import cd.casic.module.terminal.host.terminal.session.ISftpSession; +import cn.orionsec.kit.lang.id.UUIds; +import cn.orionsec.kit.lang.utils.collect.Maps; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.web.socket.WebSocketSession; + +import java.util.Map; + +/** + * sftp 设置文件内容 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2024/2/19 11:13 + */ +@Slf4j +@Component +public class SftpSetContentHandler extends AbstractTerminalHandler { + + @Resource + private TerminalRedisDAO terminalRedisDAO; + + @Override + public void handle(WebSocketSession channel, SftpBaseRequest payload) { + long startTime = System.currentTimeMillis(); + // 获取会话 + String sessionId = payload.getSessionId(); + ISftpSession session = terminalManager.getSession(channel.getId(), sessionId); + String path = payload.getPath(); + log.info("SftpSetContentHandler-handle start sessionId: {}, path: {}", sessionId, path); + String token = UUIds.random32(); + Exception ex = null; + try { + // 检查文件是否可编辑 + session.checkCanEdit(path); + // 设置缓存 + String key = TerminalCacheKeyDefine.TERMINAL_SFTP_SET_CONTENT.format(token); + SftpSetContentCacheDTO cache = SftpSetContentCacheDTO.builder() + .hostId(session.getConfig().getHostId()) + .path(path) + .build(); + terminalRedisDAO.setJson(key, TerminalCacheKeyDefine.TERMINAL_SFTP_SET_CONTENT, cache); + log.info("SftpSetContentHandler-handle success sessionId: {}, path: {}", sessionId, path); + } catch (Exception e) { + log.error("SftpSetContentHandler-handle error sessionId: {}", sessionId, e); + ex = e; + } + // 返回 + this.send(channel, + OutputTypeEnum.SFTP_SET_CONTENT, + SftpSetContentResponse.builder() + .sessionId(sessionId) + .result(BooleanBit.of(ex == null).getValue()) + .token(token) + .msg(this.getErrorMessage(ex)) + .build()); + // 保存操作日志 + Map extra = Maps.newMap(); + extra.put(OperatorLogs.PATH, path); + this.saveOperatorLog(payload, channel, + extra, TerminalOperatorType.SFTP_SET_CONTENT, + startTime, ex); + } + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/handler/SftpTouchHandler.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/handler/SftpTouchHandler.java new file mode 100644 index 00000000..4842b648 --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/handler/SftpTouchHandler.java @@ -0,0 +1,60 @@ +package cd.casic.module.terminal.host.terminal.handler; + +import cd.casic.module.terminal.common.BooleanBit; +import cd.casic.module.terminal.common.OperatorLogs; +import cd.casic.module.terminal.define.operator.TerminalOperatorType; +import cd.casic.module.terminal.host.terminal.enums.OutputTypeEnum; +import cd.casic.module.terminal.host.terminal.model.request.SftpBaseRequest; +import cd.casic.module.terminal.host.terminal.model.response.SftpBaseResponse; +import cd.casic.module.terminal.host.terminal.session.ISftpSession; +import cn.orionsec.kit.lang.utils.collect.Maps; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.web.socket.WebSocketSession; +import java.util.Map; + +/** + * sftp 创建文件 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2024/2/19 11:13 + */ +@Slf4j +@Component +public class SftpTouchHandler extends AbstractTerminalHandler { + + @Override + public void handle(WebSocketSession channel, SftpBaseRequest payload) { + long startTime = System.currentTimeMillis(); + // 获取会话 + String sessionId = payload.getSessionId(); + ISftpSession session = terminalManager.getSession(channel.getId(), sessionId); + String path = payload.getPath(); + log.info("SftpTouchHandler-handle start sessionId: {}, path: {}", sessionId, path); + Exception ex = null; + // 创建文件 + try { + session.touch(path); + log.info("SftpTouchHandler-handle success sessionId: {}, path: {}", sessionId, path); + } catch (Exception e) { + log.error("SftpTouchHandler-handle error sessionId: {}", sessionId, e); + ex = e; + } + // 返回 + this.send(channel, + OutputTypeEnum.SFTP_TOUCH, + SftpBaseResponse.builder() + .sessionId(sessionId) + .result(BooleanBit.of(ex == null).getValue()) + .msg(this.getErrorMessage(ex)) + .build()); + // 保存操作日志 + Map extra = Maps.newMap(); + extra.put(OperatorLogs.PATH, path); + this.saveOperatorLog(payload, channel, + extra, TerminalOperatorType.SFTP_TOUCH, + startTime, ex); + } + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/handler/SftpTruncateHandler.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/handler/SftpTruncateHandler.java new file mode 100644 index 00000000..7ef9df12 --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/handler/SftpTruncateHandler.java @@ -0,0 +1,61 @@ +package cd.casic.module.terminal.host.terminal.handler; + +import cd.casic.module.terminal.common.BooleanBit; +import cd.casic.module.terminal.common.OperatorLogs; +import cd.casic.module.terminal.define.operator.TerminalOperatorType; +import cd.casic.module.terminal.host.terminal.enums.OutputTypeEnum; +import cd.casic.module.terminal.host.terminal.model.request.SftpBaseRequest; +import cd.casic.module.terminal.host.terminal.model.response.SftpBaseResponse; +import cd.casic.module.terminal.host.terminal.session.ISftpSession; +import cn.orionsec.kit.lang.utils.collect.Maps; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.web.socket.WebSocketSession; + +import java.util.Map; + +/** + * sftp 截断文件 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2024/2/19 11:13 + */ +@Slf4j +@Component +public class SftpTruncateHandler extends AbstractTerminalHandler { + + @Override + public void handle(WebSocketSession channel, SftpBaseRequest payload) { + long startTime = System.currentTimeMillis(); + // 获取会话 + String sessionId = payload.getSessionId(); + ISftpSession session = terminalManager.getSession(channel.getId(), sessionId); + String path = payload.getPath(); + log.info("SftpTruncateHandler-handle start sessionId: {}, path: {}", sessionId, path); + Exception ex = null; + // 截断文件 + try { + session.truncate(path); + log.info("SftpTruncateHandler-handle success sessionId: {}, path: {}", sessionId, path); + } catch (Exception e) { + log.error("SftpTruncateHandler-handle error sessionId: {}", sessionId, e); + ex = e; + } + // 返回 + this.send(channel, + OutputTypeEnum.SFTP_TRUNCATE, + SftpBaseResponse.builder() + .sessionId(sessionId) + .result(BooleanBit.of(ex == null).getValue()) + .msg(this.getErrorMessage(ex)) + .build()); + // 保存操作日志 + Map extra = Maps.newMap(); + extra.put(OperatorLogs.PATH, path); + this.saveOperatorLog(payload, channel, + extra, TerminalOperatorType.SFTP_TRUNCATE, + startTime, ex); + } + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/handler/SshInputHandler.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/handler/SshInputHandler.java new file mode 100644 index 00000000..2c7cddde --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/handler/SshInputHandler.java @@ -0,0 +1,28 @@ +package cd.casic.module.terminal.host.terminal.handler; + +import cd.casic.module.terminal.host.terminal.model.request.SshInputRequest; +import cd.casic.module.terminal.host.terminal.session.ISshSession; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.web.socket.WebSocketSession; + +/** + * ssh 处理输入处理器 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2023/12/29 15:32 + */ +@Slf4j +@Component +public class SshInputHandler extends AbstractTerminalHandler { + + @Override + public void handle(WebSocketSession channel, SshInputRequest payload) { + // 获取会话 + ISshSession session = terminalManager.getSession(channel.getId(), payload.getSessionId()); + // 处理输入 + session.write(payload.getCommand()); + } + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/handler/SshResizeHandler.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/handler/SshResizeHandler.java new file mode 100644 index 00000000..f63a4e2f --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/handler/SshResizeHandler.java @@ -0,0 +1,28 @@ +package cd.casic.module.terminal.host.terminal.handler; + +import cd.casic.module.terminal.host.terminal.model.request.SshResizeRequest; +import cd.casic.module.terminal.host.terminal.session.ISshSession; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.web.socket.WebSocketSession; + +/** + * ssh 修改大小处理器 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2023/12/29 15:32 + */ +@Slf4j +@Component +public class SshResizeHandler extends AbstractTerminalHandler { + + @Override + public void handle(WebSocketSession channel, SshResizeRequest payload) { + // 获取会话 + ISshSession session = terminalManager.getSession(channel.getId(), payload.getSessionId()); + // 修改大小 + session.resize(payload.getCols(), payload.getRows()); + } + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/handler/TerminalCheckHandler.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/handler/TerminalCheckHandler.java new file mode 100644 index 00000000..8134cf29 --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/handler/TerminalCheckHandler.java @@ -0,0 +1,212 @@ +package cd.casic.module.terminal.host.terminal.handler; + +import cd.casic.framework.security.core.LoginUser; +import cd.casic.framework.websocket.core.util.WebSocketFrameworkUtils; +import cd.casic.module.machine.dal.dataobject.MachineInfoDO; +import cd.casic.module.machine.dal.mysql.MachineInfoMapper; +import cd.casic.module.terminal.common.*; +import cd.casic.module.terminal.dal.dataobject.TerminalConnectLogDO; +import cd.casic.module.terminal.define.operator.TerminalOperatorType; + +import cd.casic.module.terminal.controller.dto.TerminalConnectDTO; +import cd.casic.module.terminal.enums.TerminalConnectTypeEnum; +import cd.casic.module.terminal.host.terminal.constant.TerminalMessage; +import cd.casic.module.terminal.host.terminal.enums.OutputTypeEnum; +import cd.casic.module.terminal.host.terminal.enums.TerminalConnectStatusEnum; +import cd.casic.module.terminal.host.terminal.model.request.TerminalCheckRequest; +import cd.casic.module.terminal.host.terminal.model.request.host.TerminalConnectLogCreateRequest; +import cd.casic.module.terminal.host.terminal.model.response.TerminalCheckResponse; +import cd.casic.module.terminal.host.terminal.session.ITerminalSession; +import cd.casic.module.terminal.host.terminal.utils.TerminalUtils; +import cd.casic.module.terminal.service.HostConnectService; +import cd.casic.module.terminal.service.TerminalConnectLogService; +import cn.orionsec.kit.lang.exception.argument.InvalidArgumentException; +import cn.orionsec.kit.lang.utils.Exceptions; +import cn.orionsec.kit.lang.utils.collect.Maps; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.authentication.DisabledException; +import org.springframework.stereotype.Component; +import org.springframework.web.socket.WebSocketSession; +import java.util.Map; + +/** + * 终端连接检查 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2023/12/29 15:32 + */ +@Slf4j +@Component +public class TerminalCheckHandler extends AbstractTerminalHandler { + + @Resource + private MachineInfoMapper machineInfoMapper; + + @Resource + private HostConnectService hostConnectService; + + @Resource + private TerminalConnectLogService terminalConnectLogService; + + /*@Resource + private OperatorLogFrameworkService operatorLogFrameworkService;*/ + + @Override + public void handle(WebSocketSession channel, TerminalCheckRequest payload) { + Long hostId = payload.getHostId(); + + LoginUser loginUser = WebSockets.getAttr(channel, WebSocketFrameworkUtils.ATTRIBUTE_LOGIN_USER); + Long userId = loginUser.getId(); + long startTime = System.currentTimeMillis(); + TerminalConnectTypeEnum connectType = TerminalConnectTypeEnum.of(payload.getConnectType()); + String sessionId = payload.getSessionId(); + log.info("TerminalCheckHandler-handle start userId: {}, hostId: {}, sessionId: {}", loginUser.getId(), hostId, sessionId); + // 检查 session 是否存在 + if (this.checkSession(channel, payload)) { + log.info("TerminalCheckHandler-handle present session userId: {}, hostId: {}, sessionId: {}", userId, hostId, sessionId); + return; + } + // 获取主机信息 + MachineInfoDO host = this.checkHost(channel, payload, hostId); + if (host == null) { + log.info("TerminalCheckHandler-handle unknown host userId: {}, hostId: {}, sessionId: {}", userId, hostId, sessionId); + return; + } + TerminalConnectDTO connect = null; + Exception ex = null; + try { + // 获取连接信息 + connect = hostConnectService.getSshConnectInfo(host,userId); + connect.setConnectType(connectType.name()); + // 设置到缓存中 + channel.getAttributes().put(sessionId, connect); + log.info("TerminalCheckHandler-handle success userId: {}, hostId: {}, sessionId: {}", userId, hostId, sessionId); + } catch (InvalidArgumentException e) { + ex = e; + log.error("TerminalCheckHandler-handle start error userId: {}, hostId: {}, sessionId: {}", userId, hostId, sessionId, e); + } catch (DisabledException e) { + ex = Exceptions.runtime(TerminalMessage.CONFIG_DISABLED); + log.error("TerminalCheckHandler-handle disabled error userId: {}, hostId: {}, sessionId: {}", userId, hostId, sessionId); + } catch (Exception e) { + ex = Exceptions.runtime(TerminalMessage.CONNECTION_FAILED); + log.error("TerminalCheckHandler-handle exception userId: {}, hostId: {}, sessionId: {}", userId, hostId, sessionId, e); + } + // 记录主机日志 + TerminalConnectLogDO connectLog = this.saveHostLog(channel, userId, host, startTime, ex, sessionId, connectType); + if (connect != null) { + connect.setLogId(connectLog.getId()); + } + // 响应检查结果 + this.send(channel, + OutputTypeEnum.CHECK, + TerminalCheckResponse.builder() + .sessionId(payload.getSessionId()) + .result(BooleanBit.of(ex == null).getValue()) + .msg(ex == null ? null : ex.getMessage()) + .build()); + } + + /** + * 检查会话是否存在 + * + * @param channel channel + * @param payload payload + * @return 是否存在 + */ + private boolean checkSession(WebSocketSession channel, TerminalCheckRequest payload) { + ITerminalSession session = terminalManager.getSession(channel.getId(), payload.getSessionId()); + if (session != null) { + this.sendCheckFailedMessage(channel, payload, ErrorMessage.SESSION_PRESENT); + return true; + } + return false; + } + + /** + * 获取主机信息 + * + * @param channel channel + * @param payload payload + * @param hostId hostId + * @return host + */ + private MachineInfoDO checkHost(WebSocketSession channel, TerminalCheckRequest payload, Long hostId) { + // 查询主机信息 + MachineInfoDO machineDO = machineInfoMapper.selectById(hostId); +// HostMyDO host = hostDAO.selectById(hostId); + // 不存在返回错误信息 + if (machineDO == null) { + this.sendCheckFailedMessage(channel, payload, ErrorMessage.HOST_ABSENT); + } + return machineDO; + } + + /** + * 发送检查失败消息 + * + * @param channel channel + * @param payload payload + * @param msg msg + */ + private void sendCheckFailedMessage(WebSocketSession channel, TerminalCheckRequest payload, String msg) { + TerminalCheckResponse resp = TerminalCheckResponse.builder() + .sessionId(payload.getSessionId()) + .result(BooleanBit.FALSE.getValue()) + .msg(msg) + .build(); + // 发送 + this.send(channel, OutputTypeEnum.CHECK, resp); + } + + /** + * 记录主机日志 + * + * @param channel channel + * @param userId userId + * @param host host + * @param startTime startTime + * @param ex ex + * @param sessionId sessionId + * @param connectType connectType + * @return connectLog + */ + private TerminalConnectLogDO saveHostLog(WebSocketSession channel, + Long userId, + MachineInfoDO host, + long startTime, + Exception ex, + String sessionId, + TerminalConnectTypeEnum connectType) { + Long hostId = host.getId(); + String hostName = host.getName(); + String username = WebSockets.getAttr(channel, ExtraFieldConst.USERNAME); + // 额外参数 + Map extra = Maps.newMap(); + extra.put(OperatorLogs.HOST_ID, hostId); + extra.put(OperatorLogs.HOST_NAME, hostName); + extra.put(OperatorLogs.CONNECT_TYPE, connectType.name()); + extra.put(OperatorLogs.CHANNEL_ID, channel.getId()); + extra.put(OperatorLogs.SESSION_ID, sessionId); + // 日志参数 + OperatorLogModel logModel = TerminalUtils.getOperatorLogModel(channel, extra, + TerminalOperatorType.CONNECT, startTime, ex); + // 记录操作日志 +// operatorLogFrameworkService.insert(logModel); + // 记录连接日志 + TerminalConnectLogCreateRequest connectLog = TerminalConnectLogCreateRequest.builder() + .userId(userId) + .username(username) + .hostId(hostId) + .hostName(hostName) + .hostAddress(host.getHostIp()) + .status(ex == null ? TerminalConnectStatusEnum.CONNECTING.name() : TerminalConnectStatusEnum.FAILED.name()) + .sessionId(sessionId) + .extra(extra) + .build(); + // 记录连接日志 + return terminalConnectLogService.create(connectType, connectLog); + } + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/handler/TerminalCloseHandler.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/handler/TerminalCloseHandler.java new file mode 100644 index 00000000..0d857cdf --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/handler/TerminalCloseHandler.java @@ -0,0 +1,27 @@ +package cd.casic.module.terminal.host.terminal.handler; + +import cd.casic.module.terminal.host.terminal.model.TerminalBasePayload; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.web.socket.WebSocketSession; + +/** + * 关闭处理器 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2023/12/29 15:32 + */ + +@Slf4j +@Component +public class TerminalCloseHandler extends AbstractTerminalHandler { + + @Override + public void handle(WebSocketSession channel, TerminalBasePayload payload) { + log.info("TerminalCloseHandler-handle start sessionId: {}", payload.getSessionId()); + // 关闭会话 + terminalManager.closeSession(channel.getId(), payload.getSessionId()); + } + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/handler/TerminalConnectHandler.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/handler/TerminalConnectHandler.java new file mode 100644 index 00000000..b2291231 --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/handler/TerminalConnectHandler.java @@ -0,0 +1,169 @@ +package cd.casic.module.terminal.host.terminal.handler; + +import cd.casic.module.terminal.common.BooleanBit; +import cd.casic.module.terminal.common.ErrorMessage; +import cd.casic.module.terminal.common.ExtraFieldConst; +import cd.casic.module.terminal.common.WebSockets; +import cd.casic.module.terminal.define.config.AppSftpConfig; +import cd.casic.module.terminal.controller.dto.TerminalConnectDTO; +import cd.casic.module.terminal.enums.TerminalConnectTypeEnum; +import cd.casic.module.terminal.host.jsch.SessionStores; +import cd.casic.module.terminal.host.terminal.constant.TerminalMessage; +import cd.casic.module.terminal.host.terminal.enums.OutputTypeEnum; +import cd.casic.module.terminal.host.terminal.enums.TerminalConnectStatusEnum; +import cd.casic.module.terminal.host.terminal.model.TerminalConfig; +import cd.casic.module.terminal.host.terminal.model.request.TerminalConnectRequest; +import cd.casic.module.terminal.host.terminal.model.request.TerminalConnectResponse; +import cd.casic.module.terminal.host.terminal.session.ITerminalSession; +import cd.casic.module.terminal.host.terminal.session.SftpSession; +import cd.casic.module.terminal.host.terminal.session.SshSession; +import cd.casic.module.terminal.service.TerminalConnectLogService; +import cn.orionsec.kit.lang.exception.AuthenticationException; +import cn.orionsec.kit.lang.exception.TimeoutException; +import cn.orionsec.kit.lang.exception.argument.InvalidArgumentException; +import cn.orionsec.kit.lang.utils.Exceptions; +import cn.orionsec.kit.lang.utils.collect.Maps; +import cn.orionsec.kit.lang.utils.io.Streams; +import cn.orionsec.kit.net.host.SessionStore; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.web.socket.WebSocketSession; +import java.util.Map; + +/** + * 连接主机处理器 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2023/12/29 15:32 + */ +@Slf4j +@Component +public class TerminalConnectHandler extends AbstractTerminalHandler { + + @Resource + private AppSftpConfig appSftpConfig; + + @Resource + private TerminalConnectLogService terminalConnectLogService; + + @Override + public void handle(WebSocketSession channel, TerminalConnectRequest payload) { + String sessionId = payload.getSessionId(); + log.info("TerminalConnectHandler-handle start sessionId: {}", sessionId); + // 获取终端连接信息 + TerminalConnectDTO connect = WebSockets.getAttr(channel, sessionId); + if (connect == null) { + log.info("TerminalConnectHandler-handle unknown sessionId: {}", sessionId); + this.send(channel, + OutputTypeEnum.CONNECT, + TerminalConnectResponse.builder() + .sessionId(payload.getSessionId()) + .result(BooleanBit.FALSE.getValue()) + .msg(ErrorMessage.SESSION_ABSENT) + .build()); + return; + } + // 移除会话连接信息 + channel.getAttributes().remove(sessionId); + Exception ex = null; + ITerminalSession session = null; + try { + // 连接主机 + session = this.connect(sessionId, connect, channel, payload); + // 添加会话到 manager + terminalManager.addSession(session); + } catch (Exception e) { + ex = e; + Streams.close(session); + // 修改连接状态为失败 + Map extra = Maps.newMap(4); + extra.put(ExtraFieldConst.ERROR_MESSAGE, this.getConnectErrorMessage(e)); + terminalConnectLogService.updateStatusById(connect.getLogId(), TerminalConnectStatusEnum.FAILED, extra); + } + // 返回连接状态 + this.send(channel, + OutputTypeEnum.CONNECT, + TerminalConnectResponse.builder() + .sessionId(payload.getSessionId()) + .result(BooleanBit.of(ex == null).getValue()) + .msg(this.getConnectErrorMessage(ex)) + .build()); + } + + /** + * 连接主机 + * + * @param sessionId sessionId + * @param connect connect + * @param channel channel + * @param body body + * @return channel + */ + private ITerminalSession connect(String sessionId, + TerminalConnectDTO connect, + WebSocketSession channel, + TerminalConnectRequest body) { + String connectType = connect.getConnectType(); + ITerminalSession session = null; + try { + // 连接配置 + TerminalConfig config = TerminalConfig.builder() + .logId(connect.getLogId()) + .hostId(connect.getHostId()) + .hostName(connect.getHostName()) + .address(connect.getHostAddress()) + .charset(connect.getCharset()) + .fileNameCharset(connect.getFileNameCharset()) + .fileContentCharset(connect.getFileContentCharset()) + .filePreviewSize(appSftpConfig.getPreviewSize()) + .build(); + // 建立连接 + SessionStore sessionStore = SessionStores.openSessionStore(connect); + if (TerminalConnectTypeEnum.SSH.name().equals(connectType)) { + // 打开 ssh 会话 + SshSession sshSession = new SshSession(sessionId, channel, sessionStore, config); + sshSession.connect(body.getTerminalType(), body.getCols(), body.getRows()); + session = sshSession; + } else if (TerminalConnectTypeEnum.SFTP.name().equals(connectType)) { + // 打开 sftp 会话 + SftpSession sftpSession = new SftpSession(sessionId, channel, sessionStore, config); + sftpSession.connect(); + session = sftpSession; + } + log.info("TerminalConnectHandler-handle success sessionId: {}", sessionId); + return session; + } catch (Exception e) { + Streams.close(session); + log.error("TerminalConnectHandler-handle error sessionId: {}", sessionId, e); + throw e; + } + } + + /** + * 获取建立连接错误信息 + * + * @param e e + * @return errorMessage + */ + private String getConnectErrorMessage(Exception e) { + if (e == null) { + return null; + } + if (Exceptions.isCausedBy(e, TimeoutException.class)) { + // 连接超时 + return TerminalMessage.CONNECTION_TIMEOUT; + } else if (Exceptions.isCausedBy(e, AuthenticationException.class)) { + // 认证失败 + return TerminalMessage.AUTHENTICATION_FAILURE; + } else if (Exceptions.isCausedBy(e, InvalidArgumentException.class)) { + // 参数错误 + return e.getMessage(); + } else { + // 其他错误 + return TerminalMessage.SERVER_UNREACHABLE; + } + } + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/handler/TerminalPingHandler.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/handler/TerminalPingHandler.java new file mode 100644 index 00000000..32d5bc01 --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/handler/TerminalPingHandler.java @@ -0,0 +1,36 @@ +package cd.casic.module.terminal.host.terminal.handler; + +import cd.casic.module.terminal.host.terminal.enums.OutputTypeEnum; +import cd.casic.module.terminal.host.terminal.model.TerminalBasePayload; +import cd.casic.module.terminal.host.terminal.session.ITerminalSession; +import cn.orionsec.kit.lang.utils.collect.Maps; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.web.socket.WebSocketSession; +import java.util.Map; + +/** + * ping 处理器 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2023/12/29 15:32 + */ +@Slf4j +@Component +public class TerminalPingHandler extends AbstractTerminalHandler { + + @Override + public void handle(WebSocketSession channel, TerminalBasePayload payload) { + // 发送 pong + this.send(channel, OutputTypeEnum.PONG.getType()); + // 活跃 terminal + Map sessions = terminalManager.getSession(channel.getId()); + if (!Maps.isEmpty(sessions)) { + for (ITerminalSession session : sessions.values()) { + session.keepAlive(); + } + } + } + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/manager/TerminalManager.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/manager/TerminalManager.java new file mode 100644 index 00000000..d426445c --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/manager/TerminalManager.java @@ -0,0 +1,94 @@ +package cd.casic.module.terminal.host.terminal.manager; + +import cd.casic.module.terminal.host.terminal.session.ITerminalSession; +import cn.orionsec.kit.lang.define.collect.MultiConcurrentHashMap; +import cn.orionsec.kit.lang.utils.collect.Maps; +import cn.orionsec.kit.lang.utils.io.Streams; +import org.springframework.stereotype.Component; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 终端管理器 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2024/1/3 11:35 + */ +@Component +public class TerminalManager { + + /** + * 会话存储器 + */ + private final MultiConcurrentHashMap channelSessions = MultiConcurrentHashMap.create(); + + /** + * 添加会话 + * + * @param session session + */ + public void addSession(ITerminalSession session) { + channelSessions.put(session.getChannelId(), session.getSessionId(), session); + } + + /** + * 通过 channel 关闭会话 + * + * @param channelId channelId + */ + public void closeSession(String channelId) { + // 获取并移除 + ConcurrentHashMap session = channelSessions.remove(channelId); + if (!Maps.isEmpty(session)) { + session.values().forEach(Streams::close); + } + } + + /** + * 通过 channel + sessionId 关闭会话 + * + * @param channelId channelId + * @param sessionId sessionId + */ + public void closeSession(String channelId, String sessionId) { + // 获取并移除 + ITerminalSession session = channelSessions.removeElement(channelId, sessionId); + if (session != null) { + Streams.close(session); + } + } + + /** + * 获取会话 + * + * @param channelId channelId + * @param sessionId sessionId + * @param T + * @return session + */ + @SuppressWarnings("unchecked") + public T getSession(String channelId, String sessionId) { + return (T) channelSessions.get(channelId, sessionId); + } + + /** + * 获取会话 + * + * @param channelId channelId + * @return session + */ + public Map getSession(String channelId) { + return channelSessions.get(channelId); + } + + /** + * 获取全部会话 + * + * @return session + */ + public MultiConcurrentHashMap getChannelSessions() { + return channelSessions; + } + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/model/TerminalBasePayload.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/model/TerminalBasePayload.java new file mode 100644 index 00000000..ca4f5563 --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/model/TerminalBasePayload.java @@ -0,0 +1,31 @@ +package cd.casic.module.terminal.host.terminal.model; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +/** + * 终端基础 payload + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2024/1/3 21:51 + */ +@Data +@SuperBuilder +@NoArgsConstructor +@AllArgsConstructor +public class TerminalBasePayload { + + /** + * 会话id + */ + private String sessionId; + + /** + * 消息类型 + */ + private String type; + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/model/TerminalConfig.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/model/TerminalConfig.java new file mode 100644 index 00000000..5c99206f --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/model/TerminalConfig.java @@ -0,0 +1,71 @@ +package cd.casic.module.terminal.host.terminal.model; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 终端连接参数 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2024/1/3 23:30 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class TerminalConfig { + + /** + * logId + */ + private Long logId; + + /** + * 主机id + */ + private Long hostId; + + /** + * 主机名称 + */ + private String hostName; + + /** + * 主机地址 + */ + private String address; + + /** + * cols + */ + private Integer cols; + + /** + * rows + */ + private Integer rows; + + /** + * SSH输出编码 + */ + private String charset; + + /** + * 文件名称编码 + */ + private String fileNameCharset; + + /** + * 文件内容编码 + */ + private String fileContentCharset; + + /** + * 文件预览大小 + */ + private Integer filePreviewSize; + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/model/request/SftpBaseRequest.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/model/request/SftpBaseRequest.java new file mode 100644 index 00000000..be762101 --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/model/request/SftpBaseRequest.java @@ -0,0 +1,29 @@ +package cd.casic.module.terminal.host.terminal.model.request; + +import cd.casic.module.terminal.host.terminal.model.TerminalBasePayload; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +/** + * sftp 基础请求 实体对象 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2024/2/19 17:46 + */ +@Data +@SuperBuilder +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class SftpBaseRequest extends TerminalBasePayload { + + /** + * path + */ + private String path; + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/model/request/SftpChangeModeRequest.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/model/request/SftpChangeModeRequest.java new file mode 100644 index 00000000..53469d0e --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/model/request/SftpChangeModeRequest.java @@ -0,0 +1,30 @@ +package cd.casic.module.terminal.host.terminal.model.request; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +/** + * sftp 修改文件权限 实体对象 + *

+ * i|eff00a1|path|mod + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2024/2/6 13:31 + */ +@Data +@SuperBuilder +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class SftpChangeModeRequest extends SftpBaseRequest { + + /** + * 10进制的8进制 权限 + */ + private Integer mod; + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/model/request/SftpDownloadFlatDirectoryRequest.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/model/request/SftpDownloadFlatDirectoryRequest.java new file mode 100644 index 00000000..29b08829 --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/model/request/SftpDownloadFlatDirectoryRequest.java @@ -0,0 +1,30 @@ +package cd.casic.module.terminal.host.terminal.model.request; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +/** + * sftp 下载文件夹展开文件 实体对象 + *

+ * i|eff00a1|currentPath|path + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2024/2/6 13:31 + */ +@Data +@SuperBuilder +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class SftpDownloadFlatDirectoryRequest extends SftpBaseRequest { + + /** + * 当前路径 + */ + private String currentPath; + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/model/request/SftpListRequest.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/model/request/SftpListRequest.java new file mode 100644 index 00000000..56fa94aa --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/model/request/SftpListRequest.java @@ -0,0 +1,30 @@ +package cd.casic.module.terminal.host.terminal.model.request; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +/** + * sftp 列表请求 实体对象 + *

+ * i|eff00a1|path + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2024/2/6 13:31 + */ +@Data +@SuperBuilder +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class SftpListRequest extends SftpBaseRequest { + + /** + * 是否显示隐藏文件 + */ + private Integer showHiddenFile; + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/model/request/SftpMoveRequest.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/model/request/SftpMoveRequest.java new file mode 100644 index 00000000..a275f8eb --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/model/request/SftpMoveRequest.java @@ -0,0 +1,30 @@ +package cd.casic.module.terminal.host.terminal.model.request; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +/** + * sftp 移动文件 实体对象 + *

+ * i|eff00a1|path|target + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2024/2/6 13:31 + */ +@Data +@SuperBuilder +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class SftpMoveRequest extends SftpBaseRequest { + + /** + * target + */ + private String target; + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/model/request/SshInputRequest.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/model/request/SshInputRequest.java new file mode 100644 index 00000000..9aed1c39 --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/model/request/SshInputRequest.java @@ -0,0 +1,31 @@ +package cd.casic.module.terminal.host.terminal.model.request; + +import cd.casic.module.terminal.host.terminal.model.TerminalBasePayload; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +/** + * ssh 输入请求 实体对象 + *

+ * i|eff00a1|command + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2023/12/29 16:20 + */ +@Data +@SuperBuilder +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class SshInputRequest extends TerminalBasePayload { + + /** + * command + */ + private String command; + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/model/request/SshResizeRequest.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/model/request/SshResizeRequest.java new file mode 100644 index 00000000..00543e55 --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/model/request/SshResizeRequest.java @@ -0,0 +1,37 @@ +package cd.casic.module.terminal.host.terminal.model.request; + +import cd.casic.module.terminal.host.terminal.model.TerminalBasePayload; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + + +/** + * ssh 修改大小请求 实体对象 + *

+ * rs|eff00a1|100|20 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2023/12/29 16:20 + */ +@Data +@SuperBuilder +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class SshResizeRequest extends TerminalBasePayload { + + /** + * 列数 + */ + private Integer cols; + + /** + * 行数 + */ + private Integer rows; + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/model/request/TerminalCheckRequest.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/model/request/TerminalCheckRequest.java new file mode 100644 index 00000000..c52fc7ec --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/model/request/TerminalCheckRequest.java @@ -0,0 +1,37 @@ +package cd.casic.module.terminal.host.terminal.model.request; + +import cd.casic.module.terminal.host.terminal.model.TerminalBasePayload; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + + +/** + * 终端连接检查请求 实体对象 + *

+ * ck|eff00a1|1 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2023/12/29 16:20 + */ +@Data +@SuperBuilder +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class TerminalCheckRequest extends TerminalBasePayload { + + /** + * 主机id + */ + private Long hostId; + + /** + * 连接类型 + */ + private String connectType; + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/model/request/TerminalConnectRequest.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/model/request/TerminalConnectRequest.java new file mode 100644 index 00000000..1f2814ff --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/model/request/TerminalConnectRequest.java @@ -0,0 +1,42 @@ +package cd.casic.module.terminal.host.terminal.model.request; + +import cd.casic.module.terminal.host.terminal.model.TerminalBasePayload; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + + +/** + * 终端连接请求 实体对象 + *

+ * co|eff00a1|100|20 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2023/12/29 16:20 + */ +@Data +@SuperBuilder +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class TerminalConnectRequest extends TerminalBasePayload { + + /** + * 伪终端类型 + */ + private String terminalType; + + /** + * 列数 + */ + private Integer cols; + + /** + * 行数 + */ + private Integer rows; + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/model/request/TerminalConnectResponse.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/model/request/TerminalConnectResponse.java new file mode 100644 index 00000000..835bc87b --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/model/request/TerminalConnectResponse.java @@ -0,0 +1,35 @@ +package cd.casic.module.terminal.host.terminal.model.request; + +import cd.casic.module.terminal.host.terminal.model.TerminalBasePayload; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + + +/** + * 终端连接响应 实体对象 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2023/12/29 16:20 + */ +@Data +@SuperBuilder +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class TerminalConnectResponse extends TerminalBasePayload { + + /** + * 检查结果 + */ + private Integer result; + + /** + * 错误信息 + */ + private String msg; + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/model/request/host/HostTestConnectRequest.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/model/request/host/HostTestConnectRequest.java new file mode 100644 index 00000000..f34a2462 --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/model/request/host/HostTestConnectRequest.java @@ -0,0 +1,33 @@ +package cd.casic.module.terminal.host.terminal.model.request.host; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import jakarta.validation.constraints.NotNull; + +/** + * 主机 测试连接请求对象 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2023-9-11 14:16 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Schema(name = "HostTestConnectRequest", description = "主机 测试连接请求对象") +public class HostTestConnectRequest { + + @NotNull + @Schema(description = "id") + private Long id; + + @NotBlank + @Schema(description = "主机类型") + private String type; + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/model/request/host/TerminalConnectLogCreateRequest.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/model/request/host/TerminalConnectLogCreateRequest.java new file mode 100644 index 00000000..4717d5fb --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/model/request/host/TerminalConnectLogCreateRequest.java @@ -0,0 +1,50 @@ +package cd.casic.module.terminal.host.terminal.model.request.host; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Size; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import java.util.Map; + +/** + * 终端连接日志 创建请求对象 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2023-12-26 22:09 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Schema(name = "TerminalConnectLogCreateRequest", description = "终端连接日志 创建请求对象") +public class TerminalConnectLogCreateRequest { + + @Schema(description = "用户id") + private Long userId; + + @Schema(description = "用户名") + private String username; + + @Schema(description = "主机id") + private Long hostId; + + @Schema(description = "主机名称") + private String hostName; + + @Schema(description = "主机地址") + private String hostAddress; + + @Schema(description = "状态") + private String status; + + @Size(max = 64) + @Schema(description = "sessionId") + private String sessionId; + + @Schema(description = "拓展信息") + private Map extra; + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/model/response/SftpBaseResponse.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/model/response/SftpBaseResponse.java new file mode 100644 index 00000000..67852f73 --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/model/response/SftpBaseResponse.java @@ -0,0 +1,35 @@ +package cd.casic.module.terminal.host.terminal.model.response; + +import cd.casic.module.terminal.host.terminal.model.TerminalBasePayload; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + + +/** + * sftp 基础响应 实体对象 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2024/2/19 17:46 + */ +@Data +@SuperBuilder +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class SftpBaseResponse extends TerminalBasePayload { + + /** + * 结果 + */ + private Integer result; + + /** + * 消息 + */ + private String msg; + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/model/response/SftpDownloadFlatDirectoryResponse.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/model/response/SftpDownloadFlatDirectoryResponse.java new file mode 100644 index 00000000..4fd44e25 --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/model/response/SftpDownloadFlatDirectoryResponse.java @@ -0,0 +1,33 @@ +package cd.casic.module.terminal.host.terminal.model.response; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +/** + * sftp 下载文件夹展开文件 实体对象 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2024/2/6 16:20 + */ +@Data +@SuperBuilder +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class SftpDownloadFlatDirectoryResponse extends SftpBaseResponse { + + /** + * currentPath + */ + private String currentPath; + + /** + * body + */ + private String body; + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/model/response/SftpFileVO.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/model/response/SftpFileVO.java new file mode 100644 index 00000000..9d314161 --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/model/response/SftpFileVO.java @@ -0,0 +1,77 @@ +package cd.casic.module.terminal.host.terminal.model.response; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; +import java.util.Date; + +/** + * sftp 文件响应 实体对象 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2024/2/6 13:57 + */ +@Data +@SuperBuilder +@NoArgsConstructor +@AllArgsConstructor +public class SftpFileVO { + + /** + * 名称 + */ + private String name; + + /** + * 绝对路径 + */ + private String path; + + /** + * 文件后缀 + */ + private String suffix; + + /** + * 文件大小(byte) + */ + private Long size; + + /** + * 属性 + */ + private String attr; + + /** + * 是否为目录 + */ + private Boolean isDir; + + /** + * 10进制表现的8进制权限 + */ + private Integer permission; + + /** + * 用户id + */ + private Integer uid; + + /** + * 组id + */ + private Integer gid; + + /** + * 更新时间 + */ + private Date modifyTime; + + /** + * 是否可预览 + */ + private Boolean canPreview; + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/model/response/SftpGetContentResponse.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/model/response/SftpGetContentResponse.java new file mode 100644 index 00000000..a85d5f95 --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/model/response/SftpGetContentResponse.java @@ -0,0 +1,29 @@ + +package cd.casic.module.terminal.host.terminal.model.response; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +/** + * sftp 获取内容响应 实体对象 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2024/2/6 16:20 + */ +@Data +@SuperBuilder +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class SftpGetContentResponse extends SftpBaseResponse { + + /** + * content + */ + private String token; + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/model/response/SftpListResponse.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/model/response/SftpListResponse.java new file mode 100644 index 00000000..6c570ef7 --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/model/response/SftpListResponse.java @@ -0,0 +1,33 @@ +package cd.casic.module.terminal.host.terminal.model.response; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +/** + * sftp 列表响应 实体对象 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2024/2/6 16:20 + */ +@Data +@SuperBuilder +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class SftpListResponse extends SftpBaseResponse { + + /** + * path + */ + private String path; + + /** + * body + */ + private String body; + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/model/response/SftpSetContentResponse.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/model/response/SftpSetContentResponse.java new file mode 100644 index 00000000..958623eb --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/model/response/SftpSetContentResponse.java @@ -0,0 +1,29 @@ + +package cd.casic.module.terminal.host.terminal.model.response; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +/** + * sftp 设置内容响应 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2024/2/6 16:20 + */ +@Data +@SuperBuilder +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class SftpSetContentResponse extends SftpBaseResponse { + + /** + * content + */ + private String token; + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/model/response/SshOutputResponse.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/model/response/SshOutputResponse.java new file mode 100644 index 00000000..1c168f69 --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/model/response/SshOutputResponse.java @@ -0,0 +1,30 @@ +package cd.casic.module.terminal.host.terminal.model.response; + +import cd.casic.module.terminal.host.terminal.model.TerminalBasePayload; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + + +/** + * ssh 输出响应 实体对象 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2023/12/29 16:20 + */ +@Data +@SuperBuilder +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class SshOutputResponse extends TerminalBasePayload { + + /** + * body + */ + private String body; + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/model/response/TerminalCheckResponse.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/model/response/TerminalCheckResponse.java new file mode 100644 index 00000000..8843cdd4 --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/model/response/TerminalCheckResponse.java @@ -0,0 +1,35 @@ +package cd.casic.module.terminal.host.terminal.model.response; + +import cd.casic.module.terminal.host.terminal.model.TerminalBasePayload; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + + +/** + * 终端连接检查响应 实体对象 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2023/12/29 16:20 + */ +@Data +@SuperBuilder +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class TerminalCheckResponse extends TerminalBasePayload { + + /** + * 检查结果 + */ + private Integer result; + + /** + * 错误信息 + */ + private String msg; + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/model/response/TerminalCloseResponse.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/model/response/TerminalCloseResponse.java new file mode 100644 index 00000000..5847e71d --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/model/response/TerminalCloseResponse.java @@ -0,0 +1,35 @@ +package cd.casic.module.terminal.host.terminal.model.response; + +import cd.casic.module.terminal.host.terminal.model.TerminalBasePayload; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + + +/** + * 终端连接关闭响应 实体对象 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2023/12/29 16:20 + */ +@Data +@SuperBuilder +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class TerminalCloseResponse extends TerminalBasePayload { + + /** + * 是否为强制关闭 + */ + private Integer forceClose; + + /** + * 关闭信息 + */ + private String msg; + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/session/ISftpSession.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/session/ISftpSession.java new file mode 100644 index 00000000..6a219584 --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/session/ISftpSession.java @@ -0,0 +1,111 @@ +package cd.casic.module.terminal.host.terminal.session; + +import cd.casic.module.terminal.host.terminal.model.response.SftpFileVO; +import java.util.List; + +/** + * sftp 会话定义 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2024/2/4 16:48 + */ +public interface ISftpSession extends ITerminalSession { + + /** + * 建立连接 + */ + void connect(); + + /** + * 获取 home 路径 + * + * @return homePath + */ + String getHome(); + + /** + * 文件列表 + * + * @param path path + * @param showHiddenFile 是否显示隐藏文件 + * @return list + */ + List list(String path, boolean showHiddenFile); + + /** + * 创建文件夹 + * + * @param path path + */ + void mkdir(String path); + + /** + * 创建文件 + * + * @param path path + */ + void touch(String path); + + /** + * 移动文件 + * + * @param source source + * @param target target + */ + void move(String source, String target); + + /** + * 删除文件 + * + * @param paths paths + */ + void remove(String[] paths); + + /** + * 截断文件 + * + * @param path path + */ + void truncate(String path); + + /** + * 修改权限 + * + * @param path path + * @param mod mod + */ + void chmod(String path, int mod); + + /** + * 展开文件夹内的所有文件 + * + * @param paths paths + * @return files + */ + List flatDirectory(String[] paths); + + /** + * 获取内容 + * + * @param path path + * @return content + */ + String getContent(String path); + + /** + * 设置内容 + * + * @param path path + * @param content content + */ + void setContent(String path, String content); + + /** + * 检测文件是否可编辑 + * + * @param path path + */ + void checkCanEdit(String path); + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/session/ISshSession.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/session/ISshSession.java new file mode 100644 index 00000000..bf796575 --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/session/ISshSession.java @@ -0,0 +1,43 @@ +package cd.casic.module.terminal.host.terminal.session; + +/** + * ssh 会话定义 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2024/2/4 16:47 + */ +public interface ISshSession extends ITerminalSession { + + /** + * 连接 + * + * @param terminalType terminalType + * @param cols cols + * @param rows rows + */ + void connect(String terminalType, int cols, int rows); + + /** + * 重置大小 + * + * @param cols cols + * @param rows rows + */ + void resize(int cols, int rows); + + /** + * 写入内容 + * + * @param b b + */ + void write(String b); + + /** + * 写入内容 + * + * @param b b + */ + void write(byte[] b); + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/session/ITerminalSession.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/session/ITerminalSession.java new file mode 100644 index 00000000..67033aa1 --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/session/ITerminalSession.java @@ -0,0 +1,53 @@ +package cd.casic.module.terminal.host.terminal.session; + +import cd.casic.module.terminal.host.terminal.model.TerminalConfig; +import cn.orionsec.kit.lang.able.SafeCloseable; + +/** + * 终端会话定义 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2024/1/2 17:28 + */ +public interface ITerminalSession extends SafeCloseable { + + /** + * 获取 sessionId + * + * @return sessionId + */ + String getSessionId(); + + /** + * 获取 channelId + * + * @return channelId + */ + String getChannelId(); + + /** + * 获取配置 + * + * @return config + */ + TerminalConfig getConfig(); + + /** + * 活跃会话 + */ + void keepAlive(); + + /** + * 强制下线 + */ + void forceOffline(); + + /** + * 是否已关闭 + * + * @return closed + */ + boolean isClosed(); + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/session/SftpSession.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/session/SftpSession.java new file mode 100644 index 00000000..ae2f72d6 --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/session/SftpSession.java @@ -0,0 +1,216 @@ +package cd.casic.module.terminal.host.terminal.session; + + +import cd.casic.module.terminal.common.ErrorMessage; +import cd.casic.module.terminal.host.terminal.model.TerminalConfig; +import cd.casic.module.terminal.host.terminal.model.response.SftpFileVO; +import cd.casic.module.terminal.utils.Valid; +import cn.orionsec.kit.lang.constant.Const; +import cn.orionsec.kit.lang.utils.Exceptions; +import cn.orionsec.kit.lang.utils.Objects1; +import cn.orionsec.kit.lang.utils.Strings; + +import cn.orionsec.kit.lang.utils.io.FileType; +import cn.orionsec.kit.lang.utils.io.Files1; +import cn.orionsec.kit.lang.utils.io.Streams; +import cn.orionsec.kit.net.host.SessionStore; +import cn.orionsec.kit.net.host.sftp.SftpExecutor; +import cn.orionsec.kit.net.host.sftp.SftpFile; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.socket.WebSocketSession; +import java.io.InputStream; +import java.util.*; +import java.util.stream.Collectors; + +/** + * 终端 sftp 会话 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2024/1/2 17:28 + * */ + + +@Slf4j +public class SftpSession extends TerminalSession implements ISftpSession { + + private final SessionStore sessionStore; + + private SftpExecutor executor; + + public SftpSession(String sessionId, + WebSocketSession channel, + SessionStore sessionStore, + TerminalConfig config) { + super(sessionId, channel, config); + this.sessionStore = sessionStore; + } + + @Override + public void connect() { + // 打开 sftp + this.executor = sessionStore.getSftpExecutor(config.getFileNameCharset()); + executor.connect(); + } + + @Override + public String getHome() { + return executor.getHome(); + } + + @Override + public List list(String path, boolean showHiddenFile) { + path = Files1.getPath(path); + // 查询文件 + List files = executor.listFilesFilter(path, + s -> showHiddenFile || !s.getName().startsWith(Const.DOT), + false, + true); + return files.stream() + .map(this::fileMapping) + .sorted(Comparator.comparing(SftpFileVO::getName)) + .collect(Collectors.toList()); + } + + @Override + public void mkdir(String path) { + path = Valid.checkNormalize(path); + executor.makeDirectories(path); + } + + @Override + public void touch(String path) { + path = Valid.checkNormalize(path); + executor.touch(path); + } + + @Override + public void move(String source, String target) { + source = Valid.checkNormalize(source); + // 移动 + executor.move(source, target); + } + + @Override + public void remove(String[] paths) { + Arrays.stream(paths) + .map(Valid::checkNormalize) + .forEach(executor::remove); + } + + @Override + public void truncate(String path) { + path = Valid.checkNormalize(path); + executor.truncate(path); + } + + @Override + public void chmod(String path, int mod) { + path = Valid.checkNormalize(path); + executor.changeMode(path, mod); + } + + @Override + public List flatDirectory(String[] paths) { + return Arrays.stream(paths) + .map(s -> executor.listFiles(s, true, false)) + .flatMap(Collection::stream) + .map(this::fileMapping) + .collect(Collectors.toList()); + } + + @Override + public String getContent(String path) { + path = Valid.checkNormalize(path); + try { + // 获取文件 + SftpFile file = executor.getFile(path); + if (file == null || file.getSize() == 0L) { + return Const.EMPTY; + } + // 读取文件 + InputStream in = executor.openInputStream(path); + return Streams.toString(in, config.getFileContentCharset()); + } catch (Exception e) { + throw Exceptions.ioRuntime(e); + } finally { + // 关闭 inputStream 可能会被阻塞 ???...??? 只能关闭 executor + Streams.close(this.executor); + this.connect(); + } + } + + @Override + public void setContent(String path, String content) { + path = Valid.checkNormalize(path); + try { + executor.write(path, Strings.bytes(content, config.getFileContentCharset())); + } catch (Exception e) { + throw Exceptions.ioRuntime(e); + } + } + + @Override + public void checkCanEdit(String path) { + path = Valid.checkNormalize(path); + // 检查文件是否存在 + Valid.isTrue(executor.isExist(path), ErrorMessage.FILE_ABSENT); + } + + @Override + public void keepAlive() { + try { + // 发送个信号 保证 socket 不自动关闭 + executor.sendSignal(Const.EMPTY); + } catch (Exception e) { + log.error("sftp keep-alive error {}", sessionId, e); + } + } + + @Override + protected void releaseResource() { + Streams.close(executor); + Streams.close(sessionStore); + } + + /** + * 文件映射 + * + * @param sftpFile sftpFile + * @return file + * */ + + + private SftpFileVO fileMapping(SftpFile sftpFile) { + SftpFileVO file = new SftpFileVO(); + file.setName(sftpFile.getName()); + file.setPath(sftpFile.getPath()); + file.setSuffix(Files1.getSuffix(sftpFile.getName())); + file.setSize(sftpFile.getSize()); + file.setPermission(sftpFile.getPermission()); + file.setUid(sftpFile.getUid()); + file.setGid(sftpFile.getGid()); + file.setAttr(sftpFile.getPermissionString()); + file.setModifyTime(sftpFile.getModifyTime()); + Boolean isDir = Optional.ofNullable(FileType.of(file.getAttr())) + .map(FileType.DIRECTORY::equals) + .orElse(false); + file.setIsDir(isDir); + file.setCanPreview(this.calcCanPreview(sftpFile)); + return file; + } + + /** + * 检查是否可预览 + * + * @param file file + * @return canPreview + * **/ + + + private boolean calcCanPreview(SftpFile file) { + // 检查文件类型及大小 + return file.isRegularFile() && file.getSize() <= Objects1.def(config.getFilePreviewSize(), Const.N_2) * 1024 * 1024; + } + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/session/SshSession.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/session/SshSession.java new file mode 100644 index 00000000..5eed9757 --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/session/SshSession.java @@ -0,0 +1,123 @@ +package cd.casic.module.terminal.host.terminal.session; + + +import cd.casic.module.terminal.common.WebSockets; +import cd.casic.module.terminal.define.AssetThreadPools; +import cd.casic.module.terminal.host.terminal.enums.OutputTypeEnum; +import cd.casic.module.terminal.host.terminal.model.TerminalConfig; +import cd.casic.module.terminal.host.terminal.model.response.SshOutputResponse; +import cn.orionsec.kit.lang.constant.Const; +import cn.orionsec.kit.lang.utils.io.Streams; +import cn.orionsec.kit.net.host.SessionStore; +import cn.orionsec.kit.net.host.ssh.shell.ShellExecutor; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.socket.WebSocketSession; +import java.io.BufferedInputStream; +import java.io.IOException; +import java.io.InputStream; + +/** + * 终端 ssh 会话 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2024/1/2 17:28 + */ +@Slf4j +public class SshSession extends TerminalSession implements ISshSession { + + private final SessionStore sessionStore; + + private ShellExecutor executor; + + @Getter + private String lastLine; + + public SshSession(String sessionId, + WebSocketSession channel, + SessionStore sessionStore, + TerminalConfig config) { + super(sessionId, channel, config); + this.sessionStore = sessionStore; + } + + @Override + public void connect(String terminalType, int cols, int rows) { + config.setCols(cols); + config.setRows(rows); + // 打开 shell + this.executor = sessionStore.getShellExecutor(); + executor.size(cols, rows); + executor.terminalType(terminalType); + executor.streamHandler(this::streamHandler); + executor.callback(this::close); + executor.connect(); + // 开始监听输出 + AssetThreadPools.TERMINAL_STDOUT.execute(executor); + } + + @Override + public void resize(int cols, int rows) { + // 大小发生变化 则修改大小 + if (cols != config.getCols() || + rows != config.getRows()) { + config.setCols(cols); + config.setRows(rows); + executor.size(cols, rows); + executor.resize(); + } + } + + @Override + public void write(String b) { + executor.write(b); + } + + @Override + public void write(byte[] b) { + executor.write(b); + } + + @Override + public void keepAlive() { + try { + // 发送个信号 保证 socket 不自动关闭 + executor.sendSignal(Const.EMPTY); + } catch (Exception e) { + log.error("ssh keep-alive error {}", sessionId, e); + } + } + + @Override + protected void releaseResource() { + Streams.close(executor); + Streams.close(sessionStore); + } + + /** + * 标准输出处理 + * + * @param inputStream stream + */ + private void streamHandler(InputStream inputStream) { + byte[] bs = new byte[Const.BUFFER_KB_4]; + BufferedInputStream in = new BufferedInputStream(inputStream, Const.BUFFER_KB_4); + int read; + try { + while (channel.isOpen() && (read = in.read(bs)) != -1) { + String body = lastLine = new String(bs, 0, read, config.getCharset()); + // 响应 + SshOutputResponse resp = SshOutputResponse.builder() + .type(OutputTypeEnum.SSH_OUTPUT.getType()) + .sessionId(sessionId) + .body(body) + .build(); + WebSockets.sendText(channel, OutputTypeEnum.SSH_OUTPUT.format(resp)); + } + } catch (IOException ex) { + log.error("terminal 读取流失败", ex); + } + } + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/session/TerminalSession.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/session/TerminalSession.java new file mode 100644 index 00000000..6085815a --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/session/TerminalSession.java @@ -0,0 +1,114 @@ +package cd.casic.module.terminal.host.terminal.session; + +import cd.casic.module.terminal.common.BooleanBit; +import cd.casic.module.terminal.common.WebSockets; +import cd.casic.module.terminal.common.holder.SpringHolder; +import cd.casic.module.terminal.host.terminal.constant.TerminalMessage; +import cd.casic.module.terminal.host.terminal.enums.OutputTypeEnum; +import cd.casic.module.terminal.host.terminal.enums.TerminalConnectStatusEnum; +import cd.casic.module.terminal.host.terminal.model.TerminalConfig; +import cd.casic.module.terminal.host.terminal.model.response.TerminalCloseResponse; +import cd.casic.module.terminal.service.TerminalConnectLogService; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.socket.WebSocketSession; + +/** + * 终端会话基类 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2024/2/4 16:51 + */ +@Slf4j +public abstract class TerminalSession implements ITerminalSession { + + @Getter + protected final String sessionId; + + protected final WebSocketSession channel; + + @Getter + protected final TerminalConfig config; + + @Getter + protected volatile boolean closed; + + protected volatile boolean forceOffline; + + public TerminalSession(String sessionId, WebSocketSession channel, TerminalConfig config) { + this.sessionId = sessionId; + this.channel = channel; + this.config = config; + } + + /** + * 释放资源 + */ + protected abstract void releaseResource(); + + /** + * 发送关闭消息 + */ + protected void sendCloseMessage() { + log.info("TerminalSession close {}, forClose: {}, forceOffline: {}", sessionId, this.closed, this.forceOffline); + // 发送关闭信息 + TerminalCloseResponse resp = TerminalCloseResponse.builder() + .type(OutputTypeEnum.CLOSE.getType()) + .sessionId(this.sessionId) + .forceClose(BooleanBit.of(this.forceOffline).getValue()) + .msg(this.forceOffline ? TerminalMessage.FORCED_OFFLINE : TerminalMessage.CONNECTION_CLOSED) + .build(); + WebSockets.sendText(channel, OutputTypeEnum.CLOSE.format(resp)); + } + + @Override + public void close() { + log.info("terminal close {}", sessionId); + // 检查并且关闭 + if (this.checkAndClose()) { + // 修改状态 + SpringHolder.getBean(TerminalConnectLogService.class) + .updateStatusById(config.getLogId(), TerminalConnectStatusEnum.COMPLETE, null); + } + } + + @Override + public void forceOffline() { + log.info("terminal forceOffline {}", sessionId); + this.forceOffline = true; + // 关闭 + this.checkAndClose(); + } + + /** + * 检查并且关闭会话 + * + * @return close + */ + private boolean checkAndClose() { + if (closed) { + return false; + } + this.closed = true; + // 释放资源 + try { + this.releaseResource(); + } catch (Exception e) { + log.error("terminal release error {}", sessionId, e); + } + // 发送关闭信息 + try { + this.sendCloseMessage(); + } catch (Exception e) { + log.error("terminal send close error {}", sessionId, e); + } + return true; + } + + @Override + public String getChannelId() { + return channel.getId(); + } + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/utils/TerminalUtils.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/utils/TerminalUtils.java new file mode 100644 index 00000000..864ed26f --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/terminal/utils/TerminalUtils.java @@ -0,0 +1,56 @@ +package cd.casic.module.terminal.host.terminal.utils; + + +import cd.casic.module.terminal.common.OperatorLogModel; +import org.springframework.web.socket.WebSocketSession; + +import java.util.Map; + +/** + * 终端工具类 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2024/2/23 16:25 + */ +public class TerminalUtils { + + private TerminalUtils() { + } + + /** + * 获取操作日志模型 + * + * @param channel channel + * @param extra extra + * @param type type + * @param startTime startTime + * @param ex ex + * @return model + */ + public static OperatorLogModel getOperatorLogModel(WebSocketSession channel, + Map extra, + String type, + long startTime, + Exception ex) { + // 日志参数 + return null;/*OperatorLogFiller.create() + // 填充用户信息 + .fillUserInfo(WebSockets.getAttr(channel, ExtraFieldConst.USER_ID), + WebSockets.getAttr(channel, ExtraFieldConst.USERNAME)) + // 填充 traceId + .fillTraceId(WebSockets.getAttr(channel, ExtraFieldConst.TRACE_ID)) + // 填充请求留痕信息 + .fillIdentity(WebSockets.getAttr(channel, ExtraFieldConst.IDENTITY)) + // 填充使用时间 + .fillUsedTime(startTime) + // 填充结果信息 + .fillResult(null, ex) + // 填充拓展信息 + .fillExtra(extra) + // 填充日志 + .fillLogInfo(extra, type) + .get();*/ + } + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/transfer/ITransferHandler.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/transfer/ITransferHandler.java new file mode 100644 index 00000000..394d167d --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/transfer/ITransferHandler.java @@ -0,0 +1,37 @@ +package cd.casic.module.terminal.host.transfer; + + +import cn.orionsec.kit.lang.able.SafeCloseable; + +/** + * 传输处理器定义 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2024/2/21 22:46 + */ +public interface ITransferHandler extends SafeCloseable { + + /** + * 处理文本消息 + * + * @param payload payload + */ + void handleMessage(TransferOperatorRequest payload); + + /** + * 处理二进制消息 + * + * @param content content + */ + void handleMessage(byte[] content); + + /** + * 通过 token 获取 session + * + * @param token token + * @return session + */ + ITransferSession getSessionByToken(String token); + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/transfer/ITransferSession.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/transfer/ITransferSession.java new file mode 100644 index 00000000..b0a678ba --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/transfer/ITransferSession.java @@ -0,0 +1,83 @@ +package cd.casic.module.terminal.host.transfer; + + +import cn.orionsec.kit.lang.able.SafeCloseable; + +/** + * 主机传输会话定义 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2024/2/21 23:06 + */ +public interface ITransferSession extends SafeCloseable { + + /** + * 初始化 + */ + void init(); + + /** + * 处理二进制内容 + * + * @param bytes bytes + */ + void handleBinary(byte[] bytes); + + /** + * 开始传输 + * + * @param request request + */ + void onStart(TransferOperatorRequest request); + + /** + * 传输完成 + * + * @param request request + */ + void onFinish(TransferOperatorRequest request); + + /** + * 传输失败 + * + * @param request request + */ + void onError(TransferOperatorRequest request); + + /** + * 传输中断 + * + * @param request request + */ + void onAbort(TransferOperatorRequest request); + + /** + * 获取文件路径 + * + * @return path + */ + String getPath(); + + /** + * 获取 token + * + * @return token + */ + String getToken(); + + /** + * 设置 token + * + * @param token token + */ + void setToken(String token); + + /** + * 获取 hostId + * + * @return hostId + */ + Long getHostId(); + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/transfer/SftpFileBackupParams.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/transfer/SftpFileBackupParams.java new file mode 100644 index 00000000..fa0f63dc --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/transfer/SftpFileBackupParams.java @@ -0,0 +1,41 @@ +package cd.casic.module.terminal.host.transfer; + + +import cn.orionsec.kit.lang.utils.time.Dates; +import lombok.Data; + +import java.util.Date; + +/** + * sftp 文件备份参数 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2024/4/15 23:13 + */ +@Data +public class SftpFileBackupParams { + + /** + * 文件名称 + */ + private String fileName; + + /** + * 时间戳 + */ + private Long timestamp; + + /** + * 当前时间 + */ + private String time; + + public SftpFileBackupParams(String fileName) { + this.fileName = fileName; + Date date = new Date(); + this.timestamp = date.getTime(); + this.time = Dates.format(date, Dates.YMD_HMS2); + } + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/transfer/TerminalConnection.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/transfer/TerminalConnection.java new file mode 100644 index 00000000..87f330b7 --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/transfer/TerminalConnection.java @@ -0,0 +1,34 @@ +package cd.casic.module.terminal.host.transfer; + +import cd.casic.module.terminal.controller.dto.TerminalConnectDTO; +import cn.orionsec.kit.net.host.SessionStore; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + + +/** + * 终端连接信息 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2024/7/12 23:52 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class TerminalConnection { + + /** + * connectInfo + */ + private TerminalConnectDTO connectInfo; + + /** + * sessionStore + */ + private SessionStore sessionStore; + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/transfer/TerminalTransferManager.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/transfer/TerminalTransferManager.java new file mode 100644 index 00000000..63067d42 --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/transfer/TerminalTransferManager.java @@ -0,0 +1,48 @@ +package cd.casic.module.terminal.host.transfer; + +import org.springframework.stereotype.Component; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 终端传输管理器 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2024/6/4 17:58 + */ +@Component +public class TerminalTransferManager { + + private final ConcurrentHashMap handlers = new ConcurrentHashMap<>(); + + /** + * 添加处理器 + * + * @param id id + * @param handler handler + */ + public void putHandler(String id, ITransferHandler handler) { + handlers.put(id, handler); + } + + /** + * 获取处理器 + * + * @param id id + * @return handler + */ + public ITransferHandler getHandler(String id) { + return handlers.get(id); + } + + /** + * 删除处理器 + * + * @param id id + * @return handler + */ + public ITransferHandler removeHandler(String id) { + return handlers.remove(id); + } + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/transfer/TransferHandler.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/transfer/TransferHandler.java new file mode 100644 index 00000000..a4a2654b --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/transfer/TransferHandler.java @@ -0,0 +1,137 @@ +package cd.casic.module.terminal.host.transfer; + +import cd.casic.module.terminal.common.ExtraFieldConst; +import cd.casic.module.terminal.common.WebSockets; +import cd.casic.module.terminal.common.holder.SpringHolder; +import cd.casic.module.terminal.controller.dto.TerminalConnectDTO; +import cd.casic.module.terminal.host.jsch.SessionStores; +import cd.casic.module.terminal.host.transfer.session.DownloadSession; +import cd.casic.module.terminal.host.transfer.session.UploadSession; +import cd.casic.module.terminal.service.HostConnectService; +import cn.orionsec.kit.lang.id.UUIds; +import cn.orionsec.kit.lang.utils.io.Streams; +import cn.orionsec.kit.net.host.SessionStore; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.socket.WebSocketSession; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 传输处理器 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2024/2/21 20:57 + */ +@Slf4j +public class TransferHandler implements ITransferHandler { + + private static final HostConnectService hostConnectService = SpringHolder.getBean(HostConnectService.class); + + private final WebSocketSession channel; + + private ITransferSession currentSession; + + private final ConcurrentHashMap sessions; + + private final ConcurrentHashMap terminalConnections; + + public TransferHandler(WebSocketSession channel) { + this.channel = channel; + this.sessions = new ConcurrentHashMap<>(); + this.terminalConnections = new ConcurrentHashMap<>(); + } + + @Override + public void handleMessage(TransferOperatorRequest payload) { + // 解析消息类型 + TransferOperator operator = TransferOperator.of(payload.getOperator()); + // 获取会话 + if (!this.getAndInitSession(payload)) { + return; + } + // 处理消息 + if (TransferOperator.START.equals(operator)) { + // 开始传输 + currentSession.setToken(UUIds.random32()); + currentSession.onStart(payload); + } else if (TransferOperator.FINISH.equals(operator)) { + // 完成 + currentSession.onFinish(payload); + } else if (TransferOperator.ERROR.equals(operator)) { + // 失败 + currentSession.onError(payload); + } else if (TransferOperator.ABORT.equals(operator)) { + // 中断 + currentSession.onAbort(payload); + } + } + + @Override + public void handleMessage(byte[] content) { + currentSession.handleBinary(content); + } + + /** + * 获取并且初始化会话 + * + * @param payload payload + * @return success + */ + private boolean getAndInitSession(TransferOperatorRequest payload) { + Long hostId = payload.getHostId(); + TransferType type = TransferType.of(payload.getType()); + String sessionKey = hostId + "_" + type.getType(); + try { + // 获取终端连接信息 + TerminalConnection terminalConnection = terminalConnections.get(hostId); + if (terminalConnection == null) { + // 获取终端连接信息 + Long userId = WebSockets.getAttr(channel, ExtraFieldConst.USER_ID); + TerminalConnectDTO connectInfo = hostConnectService.getSshConnectInfo(hostId, userId); + terminalConnection = new TerminalConnection(connectInfo, SessionStores.openSessionStore(connectInfo)); + terminalConnections.put(hostId, terminalConnection); + } + SessionStore sessionStore = terminalConnection.getSessionStore(); + TerminalConnectDTO connectInfo = terminalConnection.getConnectInfo(); + // 获取会话 + ITransferSession session = sessions.get(sessionKey); + if (session == null) { + // 打开会话并初始化 + if (TransferType.UPLOAD.equals(type)) { + // 上传会话 + session = new UploadSession(connectInfo, sessionStore, this.channel); + } else if (TransferType.DOWNLOAD.equals(type)) { + // 下载会话 + session = new DownloadSession(connectInfo, sessionStore, this.channel); + } + session.init(); + sessions.put(sessionKey, session); + log.info("TransferHandler.getAndInitSession success channelId: {}, hostId: {}", channel.getId(), hostId); + } + this.currentSession = session; + return true; + } catch (Exception e) { + log.error("TransferHandler.getAndInitSession error channelId: {}", channel.getId(), e); + // 响应结果 + TransferUtils.sendMessage(this.channel, TransferReceiver.ERROR, e); + return false; + } + } + + @Override + public ITransferSession getSessionByToken(String token) { + return sessions.values() + .stream() + .filter(s -> token.equals(s.getToken())) + .findFirst() + .orElse(null); + } + + @Override + public void close() { + log.info("TransferHandler.close channelId: {}", channel.getId()); + sessions.values().forEach(Streams::close); + sessions.clear(); + } + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/transfer/TransferOperator.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/transfer/TransferOperator.java new file mode 100644 index 00000000..32addce4 --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/transfer/TransferOperator.java @@ -0,0 +1,53 @@ +package cd.casic.module.terminal.host.transfer; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 传输操作类型 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2024/2/21 22:03 + */ +@Getter +@AllArgsConstructor +public enum TransferOperator { + + /** + * 开始传输 + */ + START("start"), + + /** + * 传输完成 + */ + FINISH("finish"), + + /** + * 传输失败 + */ + ERROR("error"), + + /** + * 传输中断 + */ + ABORT("abort"), + + ; + + private final String type; + + public static TransferOperator of(String type) { + if (type == null) { + return null; + } + for (TransferOperator value : values()) { + if (value.type.equals(type)) { + return value; + } + } + return null; + } + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/transfer/TransferOperatorRequest.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/transfer/TransferOperatorRequest.java new file mode 100644 index 00000000..7a3812df --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/transfer/TransferOperatorRequest.java @@ -0,0 +1,46 @@ +package cd.casic.module.terminal.host.transfer; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 文件操作请求 实体对象 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2024/2/21 21:01 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class TransferOperatorRequest { + + /** + * 文件路径 + */ + private String path; + + /** + * type + */ + private String type; + + /** + * operator + */ + private String operator; + + /** + * 主机id + */ + private Long hostId; + + /** + * 错误信息 后端赋值 + */ + private String errorMessage; + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/transfer/TransferOperatorResponse.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/transfer/TransferOperatorResponse.java new file mode 100644 index 00000000..320c0fbc --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/transfer/TransferOperatorResponse.java @@ -0,0 +1,61 @@ +package cd.casic.module.terminal.host.transfer; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 文件操作响应 实体对象 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2024/2/21 22:38 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class TransferOperatorResponse { + + /** + * channelId + */ + private String channelId; + + /** + * type + */ + private String type; + + /** + * 主机id + */ + private Long hostId; + + /** + * 是否成功 + */ + private Boolean success; + + /** + * 传输的大小 + */ + private Long currentSize; + + /** + * 文件总大小 + */ + private Long totalSize; + + /** + * transferToken + */ + private String transferToken; + + /** + * 消息 + */ + private String msg; + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/transfer/TransferReceiver.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/transfer/TransferReceiver.java new file mode 100644 index 00000000..2ca11e69 --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/transfer/TransferReceiver.java @@ -0,0 +1,63 @@ +package cd.casic.module.terminal.host.transfer; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 传输响应类型 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2024/2/21 22:03 + */ +@Getter +@AllArgsConstructor +public enum TransferReceiver { + + /** + * 请求下一分片 + */ + NEXT_PART("nextPart"), + + /** + * 开始 + */ + START("start"), + + /** + * 进度 + */ + PROGRESS("progress"), + + /** + * 完成 + */ + FINISH("finish"), + + /** + * 失败 + */ + ERROR("error"), + + /** + * 关闭 + */ + ABORT("abort"), + + ; + + private final String type; + + public static TransferReceiver of(String type) { + if (type == null) { + return null; + } + for (TransferReceiver value : values()) { + if (value.type.equals(type)) { + return value; + } + } + return null; + } + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/transfer/TransferSession.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/transfer/TransferSession.java new file mode 100644 index 00000000..f9f52518 --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/transfer/TransferSession.java @@ -0,0 +1,135 @@ +package cd.casic.module.terminal.host.transfer; + +import cd.casic.module.terminal.common.OperatorLogs; +import cd.casic.module.terminal.controller.dto.TerminalConnectDTO; +import cn.orionsec.kit.lang.exception.argument.InvalidArgumentException; +import cn.orionsec.kit.lang.utils.collect.Maps; +import cn.orionsec.kit.lang.utils.io.Streams; +import cn.orionsec.kit.net.host.SessionStore; +import cn.orionsec.kit.net.host.sftp.SftpExecutor; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.socket.WebSocketSession; + +import java.util.Map; + +/** + * 主机传输会话实现 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2024/2/21 21:12 + */ +@Slf4j +public abstract class TransferSession implements ITransferSession { + + protected final TerminalConnectDTO connectInfo; + + protected final SessionStore sessionStore; + + protected final WebSocketSession channel; + + protected SftpExecutor executor; + + protected String channelId; + + @Getter + protected String path; + + @Getter + @Setter + protected String token; + + public TransferSession(TerminalConnectDTO connectInfo, SessionStore sessionStore, WebSocketSession channel) { + this.connectInfo = connectInfo; + this.sessionStore = sessionStore; + this.channel = channel; + this.channelId = channel.getId(); + } + + @Override + public void init() { + if (executor == null) { + // 建立连接 + this.executor = sessionStore.getSftpExecutor(connectInfo.getFileNameCharset()); + executor.connect(); + } else { + // 检查连接 + if (!this.executor.isConnected()) { + executor.connect(); + } + } + } + + @Override + public void handleBinary(byte[] bytes) { + } + + @Override + public void onStart(TransferOperatorRequest request) { + this.path = request.getPath(); + } + + @Override + public void onFinish(TransferOperatorRequest request) { + log.info("TransferSession.uploadFinish channelId: {}", channelId); + this.closeStream(); + // 响应结果 + TransferUtils.sendMessage(channel, TransferReceiver.FINISH, null); + } + + @Override + public void onError(TransferOperatorRequest request) { + log.error("TransferSession.uploadError channelId: {}", channelId); + this.closeStream(); + // 响应结果 + TransferUtils.sendMessage(channel, TransferReceiver.ERROR, new InvalidArgumentException(request.getErrorMessage())); + } + + @Override + public void onAbort(TransferOperatorRequest request) { + log.info("TransferSession.abort channelId: {}, path: {}", channelId, path); + // 关闭流 + this.closeStream(); + // 响应结果 + TransferUtils.sendMessage(channel, TransferReceiver.ABORT, null); + } + + /** + * 关闭流 + */ + protected abstract void closeStream(); + + @Override + public void close() { + this.closeStream(); + Streams.close(executor); + Streams.close(sessionStore); + } + + @Override + public Long getHostId() { + return connectInfo.getHostId(); + } + + /** + * 保存操作日志 + * + * @param type type + * @param path path + */ + protected void saveOperatorLog(String type, String path) { + // 设置参数 + Map extra = Maps.newMap(); + extra.put(OperatorLogs.PATH, path); + extra.put(OperatorLogs.HOST_ID, connectInfo.getHostId()); + extra.put(OperatorLogs.HOST_NAME, connectInfo.getHostName()); + extra.put(OperatorLogs.ADDRESS, connectInfo.getHostAddress()); + // 获取日志 +// OperatorLogModel model = TerminalUtils.getOperatorLogModel(this.channel, extra, type, System.currentTimeMillis(), null); + // 保存 +// SpringHolder.getBean(OperatorLogFrameworkService.class).insert(model); //先不保存日志 + } + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/transfer/TransferType.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/transfer/TransferType.java new file mode 100644 index 00000000..e5139946 --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/transfer/TransferType.java @@ -0,0 +1,43 @@ +package cd.casic.module.terminal.host.transfer; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 传输类型 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2024/7/12 12:41 + */ +@Getter +@AllArgsConstructor +public enum TransferType { + + /** + * 上传 + */ + UPLOAD("upload"), + + /** + * 下载 + */ + DOWNLOAD("download"), + + ; + + private final String type; + + public static TransferType of(String type) { + if (type == null) { + return null; + } + for (TransferType value : values()) { + if (value.type.equals(type)) { + return value; + } + } + return null; + } + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/transfer/TransferUtils.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/transfer/TransferUtils.java new file mode 100644 index 00000000..d09a16d9 --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/transfer/TransferUtils.java @@ -0,0 +1,77 @@ +package cd.casic.module.terminal.host.transfer; + +import cd.casic.module.terminal.common.ErrorMessage; +import cd.casic.module.terminal.common.WebSockets; +import cn.orionsec.kit.lang.utils.Strings; +import com.alibaba.fastjson.JSON; +import org.apache.catalina.connector.ClientAbortException; + +import org.springframework.web.socket.WebSocketSession; + +import java.util.function.Consumer; + +/** + * @author Jiahang Li + * @version 1.0.0 + * @since 2024/7/12 15:06 + */ +public class TransferUtils { + + private TransferUtils() { + } + + /** + * 发送消息 + * + * @param channel channel + * @param type type + * @param ex ex + */ + public static void sendMessage(WebSocketSession channel, TransferReceiver type, Exception ex) { + sendMessage(channel, type, ex, null); + } + + /** + * 发送消息 + * + * @param channel channel + * @param type type + * @param ex ex + * @param filler filler + */ + public static void sendMessage(WebSocketSession channel, TransferReceiver type, Exception ex, Consumer filler) { + TransferOperatorResponse resp = TransferOperatorResponse.builder() + .type(type.getType()) + .success(ex == null) + .msg(TransferUtils.getErrorMessage(ex)) + .build(); + if (filler != null) { + filler.accept(resp); + } + WebSockets.sendText(channel, JSON.toJSONString(resp)); + } + + /** + * 获取错误信息 + * + * @param ex ex + * @return msg + */ + public static String getErrorMessage(Exception ex) { + if (ex == null) { + return null; + } else if (ErrorMessage.isBizException(ex)) { + // 业务异常 + String message = ex.getMessage(); + if (Strings.isBlank(message)) { + return ErrorMessage.OPERATE_ERROR; + } + return message; + } else if (ex instanceof ClientAbortException) { + // 客户端主动断开 + return ErrorMessage.CLIENT_ABORT; + } + return ErrorMessage.OPERATE_ERROR; + } + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/transfer/session/DownloadSession.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/transfer/session/DownloadSession.java new file mode 100644 index 00000000..54ac50e2 --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/transfer/session/DownloadSession.java @@ -0,0 +1,168 @@ +package cd.casic.module.terminal.host.transfer.session; + + +import cd.casic.module.terminal.common.ErrorMessage; +import cd.casic.module.terminal.define.AssetThreadPools; +import cd.casic.module.terminal.define.operator.TerminalOperatorType; +import cd.casic.module.terminal.controller.dto.TerminalConnectDTO; +import cd.casic.module.terminal.host.transfer.TransferOperatorRequest; +import cd.casic.module.terminal.host.transfer.TransferReceiver; +import cd.casic.module.terminal.host.transfer.TransferSession; +import cd.casic.module.terminal.host.transfer.TransferUtils; +import cn.orionsec.kit.lang.constant.Const; +import cn.orionsec.kit.lang.define.wrapper.Ref; +import cn.orionsec.kit.lang.utils.Threads; +import cn.orionsec.kit.lang.utils.Valid; +import cn.orionsec.kit.lang.utils.io.Streams; +import cn.orionsec.kit.net.host.SessionStore; +import cn.orionsec.kit.net.host.sftp.SftpFile; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; +import org.springframework.web.socket.WebSocketSession; +import java.io.InputStream; +import java.io.OutputStream; + +/** + * 下载会话实现 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2024/2/22 22:25 + */ +@Slf4j +public class DownloadSession extends TransferSession implements StreamingResponseBody { + + private static final int BUFFER_SIZE = Const.BUFFER_KB_32; + + private static final int FLUSH_COUNT = Const.BUFFER_KB_1 * Const.BUFFER_KB_1 / Const.BUFFER_KB_32; + + protected InputStream inputStream; + + private Long fileSize; + + public DownloadSession(TerminalConnectDTO connectInfo, SessionStore sessionStore, WebSocketSession channel) { + super(connectInfo, sessionStore, channel); + this.fileSize = 0L; + } + + @Override + public void onStart(TransferOperatorRequest request) { + try { + super.onStart(request); + log.info("DownloadSession.startDownload open start channelId: {}, path: {}", channelId, path); + // 保存操作日志 + this.saveOperatorLog(TerminalOperatorType.SFTP_DOWNLOAD, path); + // 检查连接 + this.init(); + // 检查文件是否存在 + SftpFile file = executor.getFile(path); + Valid.notNull(file, ErrorMessage.FILE_ABSENT); + // 验证非文件夹 + Valid.isTrue(!file.isDirectory(), ErrorMessage.UNABLE_DOWNLOAD_FOLDER); + if ((this.fileSize = file.getSize()) == 0L) { + // 文件为空 + log.info("DownloadSession.startDownload file empty channelId: {}, path: {}", channelId, path); + TransferUtils.sendMessage(channel, TransferReceiver.FINISH, null); + return; + } + // 打开输入流 + this.inputStream = executor.openInputStream(path); + // 响应开始下载 + TransferUtils.sendMessage(channel, TransferReceiver.START, null, e -> { + e.setChannelId(channelId); + e.setTransferToken(token); + }); + log.info("DownloadSession.startDownload open success channelId: {}, path: {}", channelId, path); + } catch (Exception e) { + log.error("DownloadSession.startDownload open error channelId: {}, path: {}", channelId, path, e); + // 响应下载失败 + TransferUtils.sendMessage(channel, TransferReceiver.ERROR, e); + } + } + + @Override + public void onAbort(TransferOperatorRequest request) { + log.info("TransferSession.abort channelId: {}, path: {}", channelId, path); + // 关闭流 + this.closeStream(); + // download 的 abort 无需发送回调 + } + + @Override + public void writeTo(OutputStream outputStream) { + Ref ex = new Ref<>(); + try { + byte[] buffer = new byte[BUFFER_SIZE]; + int len; + int i = 0; + long size = 0; + // 响应文件内容 + while (this.inputStream != null && (len = this.inputStream.read(buffer)) != -1) { + outputStream.write(buffer, 0, len); + size += len; + // 不要每次都 flush 和 send > 1mb + if (i == FLUSH_COUNT) { + i = 0; + } + // 首次触发 + if (i == 0) { + outputStream.flush(); + this.sendProgress(size, fileSize); + } + i++; + } + // 最后一次也要 flush + if (i != 0) { + outputStream.flush(); + this.sendProgress(size, fileSize); + } + log.info("DownloadSession.download finish channelId: {}, path: {}", channelId, path); + } catch (Exception e) { + log.error("DownloadSession.download error channelId: {}, path: {}", channelId, path, e); + ex.set(e); + } + // 传输结束 异步处理 + AssetThreadPools.TERMINAL_OPERATOR.execute(() -> this.onTransferFinish(ex.getValue())); + } + + /** + * 发送进度 + * + * @param currentSize currentSize + * @param totalSize totalSize + */ + protected void sendProgress(Long currentSize, Long totalSize) { + // send + TransferUtils.sendMessage(channel, TransferReceiver.PROGRESS, null, e -> { + e.setCurrentSize(currentSize); + e.setTotalSize(totalSize); + }); + } + + /** + * 传输完成时候触发 + * + * @param e e + */ + protected void onTransferFinish(Exception e) { + // 关闭等待 jsch 内部处理 + Threads.sleep(100); + this.closeStream(); + Threads.sleep(100); + // 发送消息 + if (e == null) { + TransferUtils.sendMessage(channel, TransferReceiver.FINISH, null); + } else { + TransferUtils.sendMessage(channel, TransferReceiver.ERROR, e); + } + } + + @Override + protected void closeStream() { + // 关闭 inputStream 可能会被阻塞 ???...??? 只能关闭 executor + Streams.close(this.executor); + this.executor = null; + this.inputStream = null; + } + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/transfer/session/UploadSession.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/transfer/session/UploadSession.java new file mode 100644 index 00000000..a9388710 --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/host/transfer/session/UploadSession.java @@ -0,0 +1,81 @@ +package cd.casic.module.terminal.host.transfer.session; + +import cd.casic.module.terminal.define.operator.TerminalOperatorType; +import cd.casic.module.terminal.controller.dto.TerminalConnectDTO; +import cd.casic.module.terminal.host.transfer.TransferOperatorRequest; +import cd.casic.module.terminal.host.transfer.TransferReceiver; +import cd.casic.module.terminal.host.transfer.TransferSession; +import cd.casic.module.terminal.host.transfer.TransferUtils; +import cd.casic.module.terminal.utils.SftpUtils; +import cn.orionsec.kit.lang.utils.io.Streams; +import cn.orionsec.kit.net.host.SessionStore; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.socket.WebSocketSession; +import java.io.IOException; +import java.io.OutputStream; + +/** + * 上传会话实现 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2024/2/22 22:04 + */ +@Slf4j +public class UploadSession extends TransferSession { + + protected OutputStream outputStream; + + public UploadSession(TerminalConnectDTO connectInfo, SessionStore sessionStore, WebSocketSession channel) { + super(connectInfo, sessionStore, channel); + } + + @Override + public void onStart(TransferOperatorRequest request) { + super.onStart(request); + try { + log.info("UploadSession.startUpload start channelId: {}, path: {}", channelId, path); + // 保存操作日志 + this.saveOperatorLog(TerminalOperatorType.SFTP_UPLOAD, path); + // 检查连接 + this.init(); + // 检查文件是否存在 + SftpUtils.checkUploadFilePresent(executor, path); + // 打开输出流 + this.outputStream = executor.openOutputStream(path); + // 响应结果 + TransferUtils.sendMessage(channel, TransferReceiver.NEXT_PART, null); + log.info("UploadSession.startUpload transfer channelId: {}, path: {}", channelId, path); + } catch (Exception e) { + log.error("UploadSession.startUpload error channelId: {}, path: {}", channelId, path, e); + this.closeStream(); + // 响应结果 + TransferUtils.sendMessage(channel, TransferReceiver.ERROR, e); + } + } + + @Override + public void handleBinary(byte[] bytes) { + try { + // 写入内容 + outputStream.write(bytes); + // 响应结果 + TransferUtils.sendMessage(channel, TransferReceiver.NEXT_PART, null); + } catch (IOException e) { + log.error("UploadSession.handleBinary error channelId: {}", channel.getId(), e); + this.closeStream(); + // 响应结果 + TransferUtils.sendMessage(channel, TransferReceiver.ERROR, e); + } + } + + @Override + protected void closeStream() { + if (this.outputStream != null) { + // 关闭流 + Streams.close(outputStream); + this.outputStream = null; + } + } + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/service/HostConnectService.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/service/HostConnectService.java new file mode 100644 index 00000000..f665b678 --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/service/HostConnectService.java @@ -0,0 +1,34 @@ +package cd.casic.module.terminal.service; + + +import cd.casic.module.machine.dal.dataobject.MachineInfoDO; +import cd.casic.module.terminal.controller.dto.TerminalConnectDTO; + +/** + * 主机连接服务 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2024/10/12 23:54 + */ +public interface HostConnectService { + + /** + * 使用用户配置获取 SSH 连接信息 + * + * @param hostId hostId + * @param userId userId + * @return session + */ + TerminalConnectDTO getSshConnectInfo(Long hostId, Long userId); + + /** + * 使用用户配置获取 SSH 连接信息 + * + * @param host host + * @param userId userId + * @return session + */ + TerminalConnectDTO getSshConnectInfo(MachineInfoDO host, Long userId); + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/service/TerminalConnectLogService.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/service/TerminalConnectLogService.java new file mode 100644 index 00000000..780a6553 --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/service/TerminalConnectLogService.java @@ -0,0 +1,51 @@ +package cd.casic.module.terminal.service; + + +import cd.casic.module.terminal.dal.dataobject.TerminalConnectLogDO; +import cd.casic.module.terminal.enums.TerminalConnectTypeEnum; +import cd.casic.module.terminal.host.terminal.enums.TerminalConnectStatusEnum; +import cd.casic.module.terminal.host.terminal.model.request.host.TerminalConnectLogCreateRequest; +import java.util.List; +import java.util.Map; + +/** + * 终端连接日志 服务类 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2023-12-26 22:09 + */ +public interface TerminalConnectLogService { + + /** + * 创建终端连接日志 + * + * @param type type + * @param request request + * @return record + */ + TerminalConnectLogDO create(TerminalConnectTypeEnum type, TerminalConnectLogCreateRequest request); + + + /** + * 更新连接状态 + * + * @param id id + * @param status status + * @param extra extra + * @return effect + */ + Integer updateStatusById(Long id, TerminalConnectStatusEnum status, Map extra); + + + + /** + * 删除终端连接日志 + * + * @param idList idList + * @return effect + */ + Integer deleteTerminalConnectLog(List idList); + + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/service/TerminalSftpService.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/service/TerminalSftpService.java new file mode 100644 index 00000000..44229bcb --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/service/TerminalSftpService.java @@ -0,0 +1,48 @@ +package cd.casic.module.terminal.service; + +import org.springframework.web.multipart.MultipartFile; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; + +import java.io.IOException; + +/** + * SFTP 操作 服务类 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2023-12-26 22:09 + */ +public interface TerminalSftpService { + + /** + * 设置文件内容 + * + * @param token token + * @param response response + * @throws IOException IOException + */ + void getFileContentByToken(String token, HttpServletResponse response) throws IOException; + + /** + * 获取文件内容 + * + * @param token token + * @param file file + * @throws IOException IOException + */ + void setFileContentByToken(String token, MultipartFile file) throws IOException; + + /** + * 通过 transferToken 下载 + * + * @param channelId channelId + * @param transferToken transferToken + * @param response response + * @return body + */ + StreamingResponseBody downloadWithTransferToken(String channelId, + String transferToken, + HttpServletResponse response); + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/service/impl/HostConnectServiceImpl.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/service/impl/HostConnectServiceImpl.java new file mode 100644 index 00000000..0056ca1e --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/service/impl/HostConnectServiceImpl.java @@ -0,0 +1,158 @@ +package cd.casic.module.terminal.service.impl; + + +import cd.casic.module.machine.dal.dataobject.MachineInfoDO; +import cd.casic.module.machine.dal.dataobject.SecretKeyDO; +import cd.casic.module.machine.dal.mysql.MachineInfoMapper; +import cd.casic.module.machine.dal.mysql.SecretKeyMapper; +import cd.casic.module.terminal.common.ErrorMessage; +import cd.casic.module.terminal.controller.dto.TerminalConnectDTO; +import cd.casic.module.terminal.enums.*; +import cd.casic.module.terminal.host.config.model.HostSshConfigModel; +import cd.casic.module.terminal.host.extra.model.HostSshExtraModel; +import cd.casic.module.terminal.service.HostConnectService; +import cn.orionsec.kit.lang.utils.Valid; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import java.util.Optional; + +/** + * 主机连接服务实现 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2024/10/12 23:58 + */ +@Slf4j +@Service +public class HostConnectServiceImpl implements HostConnectService { + + + @Autowired + private MachineInfoMapper machineInfoMapper; + + @Autowired + private SecretKeyMapper secretKeyMapper; + + + + private HostSshConfigModel CONFIG = HostSshConfigModel.builder().authType(HostSshAuthTypeEnum.KEY.name()).connectTimeout(10000).charset("UTF-8").fileContentCharset("UTF-8").fileNameCharset("UTF-8").build(); + + + @Override + public TerminalConnectDTO getSshConnectInfo(Long hostId, Long userId) { + // 查询主机 + MachineInfoDO host = machineInfoMapper.selectById(hostId); + Valid.notNull(host, ErrorMessage.HOST_ABSENT); + // 获取配置 + return this.getSshConnectInfo(host, userId); + } + + + @Override + public TerminalConnectDTO getSshConnectInfo(MachineInfoDO host, Long userId) { + Long hostId = host.getId(); + log.info("HostConnectService.getSshConnectInfo hostId: {}, userId: {}", hostId, userId); + // 验证主机是否有权限 + /* List hostIdList = assetAuthorizedDataService.getUserAuthorizedHostId(userId); + Valid.isTrue(hostIdList.contains(hostId), + ErrorMessage.ANY_NO_PERMISSION, + DataPermissionTypeEnum.HOST_GROUP.getPermissionName());*/ + // 获取主机配置 + CONFIG.setPort(host.getSshPort()); + CONFIG.setUsername(host.getUsername()); + if (host.getAuthenticationType().equals(1)){ + CONFIG.setAuthType(HostSshAuthTypeEnum.PASSWORD.name()); + CONFIG.setPassword(host.getPassword()); + }else { + CONFIG.setKeyId(host.getSecretKeyId()); + } + Valid.notNull(CONFIG, ErrorMessage.CONFIG_ABSENT); + // 获取连接配置 + return this.getHostConnectInfo(host, CONFIG, null); + } + + /** + * 获取主机 SSH 连接配置 + * + * @param host host + * @param config config + * @param extra extra + * @return session + */ + private TerminalConnectDTO getHostConnectInfo(MachineInfoDO host, + HostSshConfigModel config, + HostSshExtraModel extra) { + // 填充认证信息 + TerminalConnectDTO conn = new TerminalConnectDTO(); + conn.setOsType(host.getMachineInfoType()==1? "LINUX":"WINDOWS"); +// conn.setArchType(host.getArchType()); + conn.setHostId(host.getId()); + conn.setHostName(host.getName()); +// conn.setHostCode(host.getCode()); + conn.setHostAddress(host.getHostIp()); + conn.setHostPort(config.getPort()); + conn.setTimeout(config.getConnectTimeout()); + conn.setCharset(config.getCharset()); + conn.setFileNameCharset(config.getFileNameCharset()); + conn.setFileContentCharset(config.getFileContentCharset()); + + // 获取自定义认证方式 + HostExtraSshAuthTypeEnum extraAuthType = Optional.ofNullable(extra) + .map(HostSshExtraModel::getAuthType) + .map(HostExtraSshAuthTypeEnum::of) + .orElse(null); + if (HostExtraSshAuthTypeEnum.CUSTOM_KEY.equals(extraAuthType)) { + // 自定义密钥 + config.setAuthType(HostSshAuthTypeEnum.KEY.name()); + config.setKeyId(extra.getKeyId()); + if (extra.getUsername() != null) { + config.setUsername(extra.getUsername()); + } + } else if (HostExtraSshAuthTypeEnum.CUSTOM_IDENTITY.equals(extraAuthType)) { + // 自定义身份 + config.setAuthType(HostSshAuthTypeEnum.IDENTITY.name()); + config.setIdentityId(extra.getIdentityId()); + } + + // 身份认证 + HostSshAuthTypeEnum authType = HostSshAuthTypeEnum.of(config.getAuthType()); + if (HostSshAuthTypeEnum.IDENTITY.equals(authType)) { + // 身份认证 + Valid.notNull(config.getIdentityId(), ErrorMessage.IDENTITY_ABSENT); +// HostIdentityDO identity = hostIdentityDAO.selectById(config.getIdentityId()); + Valid.notNull(host, ErrorMessage.IDENTITY_ABSENT); + config.setUsername(host.getUsername()); + HostIdentityTypeEnum identityType = HostIdentityTypeEnum.of(config.getAuthType()); + if (HostIdentityTypeEnum.PASSWORD.equals(identityType)) { + // 密码类型 + authType = HostSshAuthTypeEnum.PASSWORD; + config.setPassword(host.getPassword()); + } else if (HostIdentityTypeEnum.KEY.equals(identityType)) { + // 密钥类型 + authType = HostSshAuthTypeEnum.KEY; + config.setKeyId(host.getSecretKeyId()); + } + } + + // 填充认证信息 + conn.setUsername(config.getUsername()); + if (HostSshAuthTypeEnum.PASSWORD.equals(authType)) { + // 密码认证 + conn.setPassword(config.getPassword()); + } else if (HostSshAuthTypeEnum.KEY.equals(authType)) { + // 密钥认证 + Long keyId = config.getKeyId(); + Valid.notNull(keyId, ErrorMessage.KEY_ABSENT); + SecretKeyDO key = secretKeyMapper.selectById(keyId); + Valid.notNull(key, ErrorMessage.KEY_ABSENT); + conn.setKeyId(keyId); + conn.setPublicKey(key.getPublicKey()); + conn.setPrivateKey(key.getPrivateKey()); + conn.setPrivateKeyPassword(key.getPassword()); + } + return conn; + } + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/service/impl/TerminalConnectLogServiceImpl.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/service/impl/TerminalConnectLogServiceImpl.java new file mode 100644 index 00000000..ff9de2ba --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/service/impl/TerminalConnectLogServiceImpl.java @@ -0,0 +1,121 @@ +package cd.casic.module.terminal.service.impl; + +import cd.casic.module.terminal.common.OperatorLogs; +import cd.casic.module.terminal.convert.TerminalConnectLogConvert; +import cd.casic.module.terminal.dal.dataobject.TerminalConnectLogDO; +import cd.casic.module.terminal.dal.mysql.TerminalConnectLogMapper; +import cd.casic.module.terminal.enums.TerminalConnectTypeEnum; +import cd.casic.module.terminal.host.terminal.enums.TerminalConnectStatusEnum; +import cd.casic.module.terminal.host.terminal.manager.TerminalManager; +import cd.casic.module.terminal.host.terminal.model.request.host.TerminalConnectLogCreateRequest; +import cd.casic.module.terminal.service.TerminalConnectLogService; +import cn.orionsec.kit.lang.constant.Const; +import cn.orionsec.kit.lang.utils.collect.Lists; +import com.alibaba.fastjson.JSON; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import java.util.Date; +import java.util.List; +import java.util.Map; + +/** + * + * 终端连接日志 服务实现类 + * + * @author Yuru Pu + * @version 1.0 + * @since 2025/7/8 14:17 + */ +@Slf4j +@Service +public class TerminalConnectLogServiceImpl implements TerminalConnectLogService { + + @Autowired + private TerminalConnectLogMapper terminalConnectLogDAO; + + + @Override + public TerminalConnectLogDO create(TerminalConnectTypeEnum type, TerminalConnectLogCreateRequest request) { + TerminalConnectLogDO record = TerminalConnectLogConvert.MAPPER.to(request); + record.setType(type.name()); + String status = request.getStatus(); + record.setStatus(status); + record.setStartTime(new Date()); + record.setExtraInfo(JSON.toJSONString(request.getExtra())); + // 失败直接设置结束时间 + if (TerminalConnectStatusEnum.FAILED.name().equals(status)) { + record.setEndTime(new Date()); + } + terminalConnectLogDAO.insert(record); + return record; + } + + + @Override + public Integer updateStatusById(Long id, TerminalConnectStatusEnum status, Map partial) { + log.info("TerminalConnectLogService-updateStatusById start id: {}, status: {}", id, status); + // 查询 + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(TerminalConnectLogDO::getId, id).orderByDesc(TerminalConnectLogDO::getId); + TerminalConnectLogDO record = terminalConnectLogDAO.selectOne(queryWrapper); + /*TerminalConnectLogDO record = terminalConnectLogDAO.of() + .createWrapper() + .eq(TerminalConnectLogDO::getId, id) + .orderByDesc(TerminalConnectLogDO::getId) + .then() + .getOne();*/ + if (record == null) { + log.info("TerminalConnectLogService-updateStatusById no record id: {}", id); + return Const.N_0; + } + return this.updateStatus(record, status, partial); + } + + /** + * 更新状态 + * + * @param record record + * @param status status + * @param partial partial + * @return effect + */ + private int updateStatus(TerminalConnectLogDO record, TerminalConnectStatusEnum status, Map partial) { + // 更新 + TerminalConnectLogDO update = new TerminalConnectLogDO(); + update.setId(record.getId()); + update.setStatus(status.name()); + update.setEndTime(new Date()); + if (partial != null) { + Map extra = JSON.parseObject(record.getExtraInfo()); + if (extra == null) { + extra = partial; + } else { + extra.putAll(partial); + } + update.setExtraInfo(JSON.toJSONString(extra)); + } + return terminalConnectLogDAO.updateById(update); + } + + + @Override + @Transactional(rollbackFor = Exception.class) + public Integer deleteTerminalConnectLog(List idList) { + log.info("TerminalConnectLogService.deleteTerminalConnectLog start {}", JSON.toJSONString(idList)); + if (Lists.isEmpty(idList)) { + OperatorLogs.add(OperatorLogs.COUNT, Const.N_0); + return Const.N_0; + } + // 删除日志表 + int effect = terminalConnectLogDAO.deleteBatchIds(idList); + log.info("TerminalConnectLogService.deleteTerminalConnectLog finish {}", effect); + // 设置日志参数 + OperatorLogs.add(OperatorLogs.COUNT, effect); + return effect; + } + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/service/impl/TerminalService.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/service/impl/TerminalService.java new file mode 100644 index 00000000..111ffe73 --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/service/impl/TerminalService.java @@ -0,0 +1,57 @@ +package cd.casic.module.terminal.service.impl; + +import cd.casic.framework.security.core.LoginUser; +import cd.casic.framework.security.core.util.SecurityFrameworkUtils; +import cd.casic.module.terminal.controller.dto.TerminalAccessDTO; +import cd.casic.module.terminal.controller.dto.TerminalTransferDTO; +import cd.casic.module.terminal.dal.redis.TerminalRedisDAO; +import cd.casic.module.terminal.define.cache.TerminalCacheKeyDefine; +import cn.orionsec.kit.lang.id.UUIds; +import cn.orionsec.kit.lang.utils.Valid; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + + +/** + * 终端连接服务 + * + */ +@Slf4j +@Service +public class TerminalService{ + + + @Resource + private TerminalRedisDAO terminalRedisDAO; + + public String getTerminalAccessToken() { + LoginUser user = Valid.notNull(SecurityFrameworkUtils.getLoginUser()); + log.info("HostTerminalService.getTerminalAccessToken userId: {}", user.getId()); + String accessToken = UUIds.random19(); + TerminalAccessDTO access = TerminalAccessDTO.builder() + .userId(user.getId()) + .username(SecurityFrameworkUtils.getLoginUserNickname()) + .build(); + // 设置 access 缓存 + String key = TerminalCacheKeyDefine.TERMINAL_ACCESS.format(accessToken); + terminalRedisDAO.setJson(key, TerminalCacheKeyDefine.TERMINAL_ACCESS, access); + return accessToken; + } + + public String getTerminalTransferToken() { + + LoginUser user = Valid.notNull(SecurityFrameworkUtils.getLoginUser()); + log.info("HostTerminalService.getTerminalTransferToken userId: {}", user.getId()); + String transferToken = UUIds.random19(); + TerminalTransferDTO transfer = TerminalTransferDTO.builder() + .userId(user.getId()) + .username(SecurityFrameworkUtils.getLoginUserNickname()) + .build(); + // 设置 transfer 缓存 + String key = TerminalCacheKeyDefine.TERMINAL_TRANSFER.format(transferToken); + terminalRedisDAO.setJson(key, TerminalCacheKeyDefine.TERMINAL_TRANSFER, transfer); + return transferToken; + } + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/service/impl/TerminalSftpServiceImpl.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/service/impl/TerminalSftpServiceImpl.java new file mode 100644 index 00000000..ac781369 --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/service/impl/TerminalSftpServiceImpl.java @@ -0,0 +1,167 @@ +package cd.casic.module.terminal.service.impl; + +import cd.casic.framework.security.core.util.SecurityFrameworkUtils; +import cd.casic.module.terminal.common.Const; +import cd.casic.module.terminal.common.ErrorMessage; +import cd.casic.module.terminal.controller.dto.TerminalConnectDTO; +import cd.casic.module.terminal.dal.redis.TerminalRedisDAO; +import cd.casic.module.terminal.define.cache.TerminalCacheKeyDefine; +import cd.casic.module.terminal.host.jsch.SessionStores; +import cd.casic.module.terminal.host.terminal.dto.SftpGetContentCacheDTO; +import cd.casic.module.terminal.host.terminal.dto.SftpSetContentCacheDTO; +import cd.casic.module.terminal.host.transfer.TerminalTransferManager; +import cd.casic.module.terminal.host.transfer.session.DownloadSession; +import cd.casic.module.terminal.service.HostConnectService; +import cd.casic.module.terminal.service.TerminalSftpService; +import cn.hutool.core.io.IoUtil; +import cn.orionsec.kit.lang.constant.StandardContentType; +import cn.orionsec.kit.lang.constant.StandardHttpHeader; +import cn.orionsec.kit.lang.define.wrapper.HttpWrapper; +import cn.orionsec.kit.lang.utils.Exceptions; +import cn.orionsec.kit.lang.utils.Strings; +import cn.orionsec.kit.lang.utils.Valid; +import cn.orionsec.kit.lang.utils.io.Files1; +import cn.orionsec.kit.lang.utils.io.Streams; +import cn.orionsec.kit.net.host.SessionStore; +import cn.orionsec.kit.net.host.sftp.SftpExecutor; +import jakarta.annotation.Resource; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.Optional; + +/** + * SFTP 操作 服务实现类 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2024/3/4 23:35 + */ +@Slf4j +@Service +public class TerminalSftpServiceImpl implements TerminalSftpService { + + @Resource + private TerminalRedisDAO terminalRedisDAO; + + @Resource + private HostConnectService hostConnectService; + + @Resource + private TerminalTransferManager terminalTransferManager; + + @Override + public void getFileContentByToken(String token, HttpServletResponse response) throws IOException { + InputStream in = null; + // 解析 token + String key = TerminalCacheKeyDefine.TERMINAL_SFTP_GET_CONTENT.format(token); + SftpGetContentCacheDTO cache = terminalRedisDAO.getJson(key, TerminalCacheKeyDefine.TERMINAL_SFTP_GET_CONTENT); + if (cache == null) { +// Servlets.writeHttpWrapper(response, HttpWrapper.error(ErrorMessage.FILE_ABSENT)); + HttpWrapper wrapper = HttpWrapper.error(ErrorMessage.FILE_ABSENT); + in = Streams.toInputStream(wrapper.toJsonString().getBytes(StandardCharsets.UTF_8)); + response.setContentType(StandardContentType.APPLICATION_JSON_UTF8); + IoUtil.copy(in, response.getOutputStream()); + return; + } + // 删除缓存 + terminalRedisDAO.delete(key); + // 获取文件内容 + SessionStore sessionStore = null; + SftpExecutor executor = null; + try { + // 获取终端连接信息 + TerminalConnectDTO connectInfo = hostConnectService.getSshConnectInfo(cache.getHostId(), SecurityFrameworkUtils.getLoginUser().getId()); + sessionStore = SessionStores.openSessionStore(connectInfo); + executor = sessionStore.getSftpExecutor(connectInfo.getFileNameCharset()); + executor.connect(); + // 读取文件 + in = executor.openInputStream(cache.getPath()); + // 设置返回 +// Servlets.setContentType(response, StandardContentType.TEXT_PLAIN); +// Servlets.transfer(response, in); + response.setContentType(StandardContentType.TEXT_PLAIN); + IoUtil.copy(in, response.getOutputStream()); + } catch (Exception e) { +// Servlets.writeHttpWrapper(response, HttpWrapper.error(ErrorMessage.FILE_READ_ERROR)); + HttpWrapper wrapper = HttpWrapper.error(ErrorMessage.FILE_READ_ERROR); + in = Streams.toInputStream(wrapper.toJsonString().getBytes(StandardCharsets.UTF_8)); + response.setContentType(StandardContentType.APPLICATION_JSON_UTF8); + IoUtil.copy(in, response.getOutputStream()); + } finally { + IoUtil.close(in); + Streams.close(executor); + Streams.close(sessionStore); + Streams.close(in); + } + } + + @Override + public void setFileContentByToken(String token, MultipartFile file) { + // 解析 token + String key = TerminalCacheKeyDefine.TERMINAL_SFTP_SET_CONTENT.format(token); + SftpSetContentCacheDTO cache = terminalRedisDAO.getJson(key, TerminalCacheKeyDefine.TERMINAL_SFTP_SET_CONTENT); + Valid.notNull(cache, ErrorMessage.FILE_ABSENT); + // 删除缓存 + terminalRedisDAO.delete(key); + // 写入文件内容 + SessionStore sessionStore = null; + SftpExecutor executor = null; + OutputStream out = null; + InputStream in = null; + try { + // 获取终端连接信息 + TerminalConnectDTO connectInfo = hostConnectService.getSshConnectInfo(cache.getHostId(), SecurityFrameworkUtils.getLoginUser().getId()); + sessionStore = SessionStores.openSessionStore(connectInfo); + executor = sessionStore.getSftpExecutor(connectInfo.getFileNameCharset()); + executor.connect(); + // 写入文件 + out = executor.openOutputStream(cache.getPath()); + Streams.transfer(in = file.getInputStream(), out); + } catch (Exception e) { + throw Exceptions.app(ErrorMessage.OPERATE_ERROR); + } finally { + Streams.close(executor); + Streams.close(sessionStore); + Streams.close(out); + Streams.close(in); + } + } + + @Override + public StreamingResponseBody downloadWithTransferToken(String channelId, String transferToken, HttpServletResponse response) { + // 获取会话 + DownloadSession session = (DownloadSession) Optional.ofNullable(channelId) + .map(terminalTransferManager::getHandler) + .map(s -> s.getSessionByToken(transferToken)) + .filter(s -> s instanceof DownloadSession) + .orElse(null); + // 响应会话 + if (session == null) { + response.setContentType(StandardContentType.TEXT_HTML); + response.setCharacterEncoding(Const.UTF_8); + return outputStream -> outputStream.write(Strings.bytes(ErrorMessage.SESSION_ABSENT)); + } + // 响应文件 + setAttachmentHeader(response, Files1.getFileName(session.getPath())); + return session; + } + + public void setAttachmentHeader(HttpServletResponse response, String fileName) { + response.setContentType(StandardContentType.APPLICATION_STREAM); + response.setHeader(StandardHttpHeader.ACCESS_CONTROL_EXPOSE_HEADERS, StandardHttpHeader.CONTENT_DISPOSITION); + try { + response.setHeader(StandardHttpHeader.CONTENT_DISPOSITION, "attachment; filename=" + URLEncoder.encode(fileName, cn.orionsec.kit.lang.constant.Const.UTF_8)); + } catch (UnsupportedEncodingException e) { + throw Exceptions.unsupportedEncoding(e); + } + } +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/utils/RsaParamDecryptUtils.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/utils/RsaParamDecryptUtils.java new file mode 100644 index 00000000..94d8d64d --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/utils/RsaParamDecryptUtils.java @@ -0,0 +1,38 @@ +package cd.casic.module.terminal.utils; + +import cd.casic.module.terminal.common.RsaDecryptor; +import cn.orionsec.kit.lang.utils.Exceptions; + +/** + * rsa 参数解密工具类 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2025/1/5 21:13 + */ +public class RsaParamDecryptUtils { + + private static RsaDecryptor delegate; + + private RsaParamDecryptUtils() { + } + + /** + * 解密 + * + * @param value value + * @return value + */ + public static String decrypt(String value) { + return delegate.decrypt(value); + } + + public static void setDelegate(RsaDecryptor delegate) { + if (RsaParamDecryptUtils.delegate != null) { + // unmodified + throw Exceptions.state(); + } + RsaParamDecryptUtils.delegate = delegate; + } + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/utils/SftpUtils.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/utils/SftpUtils.java new file mode 100644 index 00000000..6ab463ee --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/utils/SftpUtils.java @@ -0,0 +1,50 @@ +package cd.casic.module.terminal.utils; + +import cd.casic.module.terminal.common.holder.SpringHolder; +import cd.casic.module.terminal.define.config.AppSftpConfig; +import cd.casic.module.terminal.host.transfer.SftpFileBackupParams; +import cn.orionsec.kit.lang.utils.Booleans; +import cn.orionsec.kit.lang.utils.Strings; +import cn.orionsec.kit.net.host.sftp.SftpExecutor; +import cn.orionsec.kit.net.host.sftp.SftpFile; +import com.alibaba.fastjson.JSON; + + + +/** + * sftp 工具类 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2024/5/8 16:17 + */ +public class SftpUtils { + + private static final AppSftpConfig appSftpConfig = SpringHolder.getBean(AppSftpConfig.class); + + private SftpUtils() { + } + + /** + * 检查上传文件是否存在 并且执行响应策略 + * + * @param executor executor + * @param path path + */ + public static void checkUploadFilePresent(SftpExecutor executor, String path) { + // 重复不备份 + if (!Booleans.isTrue(appSftpConfig.getUploadPresentBackup())) { + return; + } + // 检查文件是否存在 + SftpFile file = executor.getFile(path); + if (file != null) { + // 文件存在则备份 + SftpFileBackupParams backupParams = new SftpFileBackupParams(file.getName()); + String target = Strings.format(appSftpConfig.getUploadBackupFileName(), JSON.parseObject(JSON.toJSONString(backupParams))); + // 移动 + executor.move(path, target); + } + } + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/utils/Valid.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/utils/Valid.java new file mode 100644 index 00000000..91be5bb2 --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/utils/Valid.java @@ -0,0 +1,140 @@ +package cd.casic.module.terminal.utils; + +import cd.casic.module.terminal.common.ErrorMessage; +import cd.casic.module.terminal.common.holder.SpringHolder; +import cn.orionsec.kit.lang.utils.Arrays1; +import cn.orionsec.kit.lang.utils.io.Files1; +import jakarta.validation.ConstraintViolation; +import jakarta.validation.ConstraintViolationException; +import jakarta.validation.Validator; + +import java.util.Collection; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; + +/** + * 验证器 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2023/7/18 11:23 + */ +public class Valid extends cn.orionsec.kit.lang.utils.Valid { + + private static final Validator validator = SpringHolder.getBean(Validator.class); + + public static T notNull(T object) { + return notNull(object, ErrorMessage.PARAM_MISSING); + } + + public static T isNull(T object) { + return isNull(object, ErrorMessage.PARAM_ERROR); + } + + public static String notBlank(String s) { + return notBlank(s, ErrorMessage.PARAM_MISSING); + } + + public static > T notEmpty(T map) { + return notEmpty(map, ErrorMessage.PARAM_MISSING); + } + + public static > T notEmpty(T object) { + return notEmpty(object, ErrorMessage.PARAM_MISSING); + } + + public static void allNotNull(Object... objects) { + if (objects != null) { + for (Object t : objects) { + notNull(t, ErrorMessage.PARAM_MISSING); + } + } + } + + public static void allNotBlank(String... ss) { + if (ss != null) { + for (String s : ss) { + notBlank(s, ErrorMessage.PARAM_MISSING); + } + } + } + + public static void eq(Object o1, Object o2) { + eq(o1, o2, ErrorMessage.INVALID_PARAM); + } + + public static boolean isTrue(boolean s) { + return isTrue(s, ErrorMessage.INVALID_PARAM); + } + + public static boolean isFalse(boolean s) { + return isFalse(s, ErrorMessage.INVALID_PARAM); + } + + public static > T gte(T t1, T t2) { + return gte(t1, t2, ErrorMessage.INVALID_PARAM); + } + + @SafeVarargs + public static T in(T t, T... ts) { + notNull(t, ErrorMessage.INVALID_PARAM); + notEmpty(ts, ErrorMessage.INVALID_PARAM); + isTrue(Arrays1.contains(ts, t), ErrorMessage.INVALID_PARAM); + return t; + } + + /** + * 验证枚举 + * + * @param of of method + * @param obj obj + * @param param + * @param enum + * @return enum + */ + public static > E valid(Function of, T obj) { + return notNull(of.apply(obj), ErrorMessage.INVALID_PARAM); + } + + /** + * 验证对象 + * + * @param obj obj + * @param groups groups + * @param T + * @return obj + */ + public static T valid(T obj, Class... groups) { + notNull(obj, ErrorMessage.PARAM_MISSING); + // 验证对象 + Set> set = validator.validate(obj, groups); + if (!set.isEmpty()) { + throw new ConstraintViolationException(set); + } + return obj; + } + + /** + * 检查是否更新成功 + * + * @param effect effect + * @return effect + */ + public static int version(int effect) { + isTrue(effect > 0, ErrorMessage.DATA_MODIFIED); + return effect; + } + + /** + * 检查路径是否合法化 即不包含 ./ ../ + * + * @param path path + */ + public static String checkNormalize(String path) { + Valid.notBlank(path); + Valid.isTrue(Files1.isNormalize(path), ErrorMessage.PATH_NOT_NORMALIZE); + return Files1.getPath(path); + } + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/websocket/SFTPWebSocketMessageListener.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/websocket/SFTPWebSocketMessageListener.java new file mode 100644 index 00000000..a4dac0eb --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/websocket/SFTPWebSocketMessageListener.java @@ -0,0 +1,45 @@ +package cd.casic.module.terminal.websocket; + +import cd.casic.framework.websocket.core.listener.WebSocketMessageListener; +import cd.casic.module.terminal.host.transfer.ITransferHandler; +import cd.casic.module.terminal.host.transfer.TerminalTransferManager; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.web.socket.*; + + +/** + * sftp 传输消息处理器 接收二进制消息 + * + * @author Yuru Pu + * @version 1.0 + * @since 2025/7/4 16:28 + */ +@Slf4j +@Component +public class SFTPWebSocketMessageListener implements WebSocketMessageListener{ + + @Resource + private TerminalTransferManager terminalTransferManager; + + + @Override + public void onMessage(WebSocketSession session, BinaryMessage message) { + handleBinaryMessage(session,message); + } + + @Override + public String getType() { + return "SFTP-access-message-send"; + } + + + protected void handleBinaryMessage(WebSocketSession session, BinaryMessage message) {//3 + // 获取处理器 + ITransferHandler handler = terminalTransferManager.getHandler(session.getId()); + // 添加数据 + handler.handleMessage(message.getPayload().array()); + } + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/websocket/TerminalWebSocketMessageListener.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/websocket/TerminalWebSocketMessageListener.java new file mode 100644 index 00000000..d8d0b2f0 --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/websocket/TerminalWebSocketMessageListener.java @@ -0,0 +1,70 @@ +package cd.casic.module.terminal.websocket; + +import cd.casic.framework.websocket.core.listener.WebSocketMessageListener; +import cd.casic.module.terminal.define.AssetThreadPools; +import cd.casic.module.terminal.host.terminal.enums.InputTypeEnum; +import cd.casic.module.terminal.host.terminal.manager.TerminalManager; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.web.socket.CloseStatus; +import org.springframework.web.socket.TextMessage; +import org.springframework.web.socket.WebSocketSession; +import org.springframework.web.socket.handler.AbstractWebSocketHandler; + + +/** + * 终端处理器 + * + * @author Yuru Pu + * @version 1.0 + * @since 2025/7/4 16:28 + */ +@Slf4j +@Component +public class TerminalWebSocketMessageListener implements WebSocketMessageListener { + + @Resource + private TerminalManager terminalManager; + @Override + public void onMessage(WebSocketSession session, TextMessage message) { + handleTextMessage(session, message); + } + + @Override + public String getType() { + return "terminal-access-message-send"; + } + + + protected void handleTextMessage(WebSocketSession session, TextMessage message) { + String payload = message.getPayload(); + try { + // 解析类型 + InputTypeEnum type = InputTypeEnum.of(payload); + if (type == null) { + return; + } + // 解析并处理消息 + if (type.isAsyncExec()) { + // 异步执行 + AssetThreadPools.TERMINAL_OPERATOR.execute(() -> { + type.getHandler().handle(session, type.parse(payload)); + }); + } else { + // 同步执行 + type.getHandler().handle(session, type.parse(payload)); + } + } catch (Exception e) { + log.error("TerminalDispatchHandler-handleMessage-error id: {}, msg: {}", session.getId(), payload, e); + } + } + + public void afterConnectionClosed(WebSocketSession session, CloseStatus status) { + String id = session.getId(); + log.info("TerminalMessageDispatcher-afterConnectionClosed id: {}, code: {}, reason: {}", id, status.getCode(), status.getReason()); + // 关闭会话 + terminalManager.closeSession(id); + } + +} diff --git a/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/websocket/TransferWebSocketMessageListener.java b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/websocket/TransferWebSocketMessageListener.java new file mode 100644 index 00000000..7ac135df --- /dev/null +++ b/modules/module-ci-terminal/src/main/java/cd/casic/module/terminal/websocket/TransferWebSocketMessageListener.java @@ -0,0 +1,69 @@ +package cd.casic.module.terminal.websocket; + +import cd.casic.framework.websocket.core.listener.WebSocketMessageListener; +import cd.casic.framework.websocket.core.session.WebSocketSessionHandlerDecorator; +import cd.casic.framework.websocket.core.session.WebSocketSessionManager; +import cd.casic.module.terminal.host.transfer.ITransferHandler; +import cd.casic.module.terminal.host.transfer.TerminalTransferManager; +import cd.casic.module.terminal.host.transfer.TransferHandler; +import cd.casic.module.terminal.host.transfer.TransferOperatorRequest; +import cn.orionsec.kit.lang.utils.io.Streams; +import com.alibaba.fastjson.JSON; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.web.socket.CloseStatus; +import org.springframework.web.socket.TextMessage; +import org.springframework.web.socket.WebSocketHandler; +import org.springframework.web.socket.WebSocketSession; + + + +/** + * sftp 传输消息处理器 文本处理 + * + * @author Yuru Pu + * @version 1.0 + * @since 2025/7/4 16:28 + */ +@Slf4j +@Component +public class TransferWebSocketMessageListener implements WebSocketMessageListener{ + + @Resource + private TerminalTransferManager terminalTransferManager; + + + @Override + public void onMessage(WebSocketSession session, TextMessage message) { + afterConnectionEstablished(session); + handleTextMessage(session, message); + } + + @Override + public String getType() { + return "transfer-access-message-send"; + } + + + public void afterConnectionEstablished(WebSocketSession session) { + log.info("TransferMessageHandler-afterConnectionEstablished id: {}", session.getId()); + // 添加处理器 + if (terminalTransferManager.getHandler(session.getId())==null) { + terminalTransferManager.putHandler(session.getId(), new TransferHandler(session)); + } + } + + protected void handleTextMessage(WebSocketSession session, TextMessage message) { + // 获取处理器 + ITransferHandler handler = terminalTransferManager.getHandler(session.getId()); + // 处理消息 + handler.handleMessage(JSON.parseObject(message.getPayload(), TransferOperatorRequest.class)); + } + public void afterConnectionClosed(WebSocketSession session, CloseStatus status) { + String id = session.getId(); + log.info("TransferMessageHandler-afterConnectionClosed id: {}, code: {}, reason: {}", id, status.getCode(), status.getReason()); + // 关闭会话 + Streams.close(terminalTransferManager.removeHandler(id)); + } +} diff --git a/modules/pom.xml b/modules/pom.xml index d5849dd3..d9f40771 100644 --- a/modules/pom.xml +++ b/modules/pom.xml @@ -15,6 +15,7 @@ module-system-api module-system-biz module-ci-machine + module-ci-terminal modules diff --git a/ops-server/pom.xml b/ops-server/pom.xml index d3f33ed9..74fbf3fa 100644 --- a/ops-server/pom.xml +++ b/ops-server/pom.xml @@ -30,6 +30,11 @@ module-ci-machine ${revision} + + cd.casic.boot + module-ci-terminal + ${revision} + cd.casic.boot