diff --git a/docker-compose.yml b/docker-compose.yml index 3f0b14f0..a1f6fd61 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,6 @@ version: '3.3' -# latest = 2.4.0 +# latest = 2.4.1 services: ui: image: registry.cn-hangzhou.aliyuncs.com/orionsec/orion-visor-ui:latest diff --git a/docker/adminer/build.sh b/docker/adminer/build.sh index 946851b1..714410f5 100644 --- a/docker/adminer/build.sh +++ b/docker/adminer/build.sh @@ -1,6 +1,6 @@ #/bin/bash set -e -version=2.4.0 +version=2.4.1 docker build -t orion-visor-adminer:${version} . docker tag orion-visor-adminer:${version} registry.cn-hangzhou.aliyuncs.com/orionsec/orion-visor-adminer:${version} docker tag orion-visor-adminer:${version} registry.cn-hangzhou.aliyuncs.com/orionsec/orion-visor-adminer:latest diff --git a/docker/guacd/Dockerfile b/docker/guacd/Dockerfile index eb639087..5bbaf75c 100644 --- a/docker/guacd/Dockerfile +++ b/docker/guacd/Dockerfile @@ -1,4 +1,4 @@ -FROM guacamole/guacd:1.5.5 +FROM guacamole/guacd:1.6.0 USER root # 系统时区 ARG TZ=Asia/Shanghai diff --git a/docker/guacd/build.sh b/docker/guacd/build.sh index fab46a88..0597bab2 100644 --- a/docker/guacd/build.sh +++ b/docker/guacd/build.sh @@ -1,6 +1,6 @@ #/bin/bash set -e -version=2.4.0 +version=2.4.1 docker build -t orion-visor-guacd:${version} . docker tag orion-visor-guacd:${version} registry.cn-hangzhou.aliyuncs.com/orionsec/orion-visor-guacd:${version} docker tag orion-visor-guacd:${version} registry.cn-hangzhou.aliyuncs.com/orionsec/orion-visor-guacd:latest diff --git a/docker/mysql/build.sh b/docker/mysql/build.sh index b521946d..37bf3f45 100644 --- a/docker/mysql/build.sh +++ b/docker/mysql/build.sh @@ -1,6 +1,6 @@ #/bin/bash set -e -version=2.4.0 +version=2.4.1 cp -r ../../sql ./sql docker build -t orion-visor-mysql:${version} . rm -rf ./sql diff --git a/docker/push.sh b/docker/push.sh index 7b853ab1..2de73c74 100644 --- a/docker/push.sh +++ b/docker/push.sh @@ -1,6 +1,6 @@ #/bin/bash set -e -version=2.4.0 +version=2.4.1 docker push registry.cn-hangzhou.aliyuncs.com/orionsec/orion-visor-adminer:${version} docker push registry.cn-hangzhou.aliyuncs.com/orionsec/orion-visor-mysql:${version} docker push registry.cn-hangzhou.aliyuncs.com/orionsec/orion-visor-redis:${version} diff --git a/docker/redis/build.sh b/docker/redis/build.sh index 57be5160..f0a54c46 100644 --- a/docker/redis/build.sh +++ b/docker/redis/build.sh @@ -1,6 +1,6 @@ #/bin/bash set -e -version=2.4.0 +version=2.4.1 docker build -t orion-visor-redis:${version} . docker tag orion-visor-redis:${version} registry.cn-hangzhou.aliyuncs.com/orionsec/orion-visor-redis:${version} docker tag orion-visor-redis:${version} registry.cn-hangzhou.aliyuncs.com/orionsec/orion-visor-redis:latest diff --git a/docker/service/build.sh b/docker/service/build.sh index 154ed210..f8dd6b97 100644 --- a/docker/service/build.sh +++ b/docker/service/build.sh @@ -1,6 +1,6 @@ #/bin/bash set -e -version=2.4.0 +version=2.4.1 mv ../../orion-visor-launch/target/orion-visor-launch.jar ./orion-visor-launch.jar docker build -t orion-visor-service:${version} . rm -rf ./orion-visor-launch.jar diff --git a/docker/ui/build.sh b/docker/ui/build.sh index b925c4cc..471297e2 100644 --- a/docker/ui/build.sh +++ b/docker/ui/build.sh @@ -1,6 +1,6 @@ #/bin/bash set -e -version=2.4.0 +version=2.4.1 mv ../../orion-visor-ui/dist ./dist docker build -t orion-visor-ui:${version} . rm -rf ./orion-visor-launch.jar diff --git a/orion-visor-common/src/main/java/org/dromara/visor/common/constant/AppConst.java b/orion-visor-common/src/main/java/org/dromara/visor/common/constant/AppConst.java index 1dd49d18..133ecb40 100644 --- a/orion-visor-common/src/main/java/org/dromara/visor/common/constant/AppConst.java +++ b/orion-visor-common/src/main/java/org/dromara/visor/common/constant/AppConst.java @@ -36,7 +36,7 @@ public interface AppConst extends OrionConst { /** * 同 ${orion.version} 迭代时候需要手动更改 */ - String VERSION = "2.4.0"; + String VERSION = "2.4.1"; /** * 同 ${spring.application.name} diff --git a/orion-visor-common/src/main/java/org/dromara/visor/common/constant/HttpHeaderConst.java b/orion-visor-common/src/main/java/org/dromara/visor/common/constant/HttpHeaderConst.java new file mode 100644 index 00000000..3e87a5ea --- /dev/null +++ b/orion-visor-common/src/main/java/org/dromara/visor/common/constant/HttpHeaderConst.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2023 - present Dromara, All rights reserved. + * + * https://visor.dromara.org + * https://visor.dromara.org.cn + * https://visor.orionsec.cn + * + * Members: + * Jiahang Li - ljh1553488six@139.com - author + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dromara.visor.common.constant; + +import cn.orionsec.kit.lang.constant.StandardHttpHeader; + +/** + * http 请求头 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2025/7/1 1:02 + */ +public interface HttpHeaderConst extends StandardHttpHeader { + + String APP_VERSION = "X-App-Version"; + +} diff --git a/orion-visor-common/src/main/java/org/dromara/visor/common/session/config/RdpConnectConfig.java b/orion-visor-common/src/main/java/org/dromara/visor/common/session/config/RdpConnectConfig.java index 8a916894..f40e42f3 100644 --- a/orion-visor-common/src/main/java/org/dromara/visor/common/session/config/RdpConnectConfig.java +++ b/orion-visor-common/src/main/java/org/dromara/visor/common/session/config/RdpConnectConfig.java @@ -47,6 +47,9 @@ public class RdpConnectConfig extends BaseConnectConfig { @Schema(description = "低带宽模式") private Boolean lowBandwidthMode; + @Schema(description = "初始化程序") + private String initialProgram; + @Schema(description = "RDP 版本是否大于8.1") private Boolean versionGt81; diff --git a/orion-visor-dependencies/pom.xml b/orion-visor-dependencies/pom.xml index f617b4c6..7dc53199 100644 --- a/orion-visor-dependencies/pom.xml +++ b/orion-visor-dependencies/pom.xml @@ -14,7 +14,7 @@ https://github.com/dromara/orion-visor - 2.4.0 + 2.4.1 2.7.17 2.7.15 1.5.0 @@ -34,7 +34,7 @@ 4.11.0 1.0.7 7.2.11.RELEASE - 1.5.5 + 1.6.0 diff --git a/orion-visor-launch/src/test/java/org/dromara/visor/launch/ReplaceVersion.java b/orion-visor-launch/src/test/java/org/dromara/visor/launch/ReplaceVersion.java index 4f675802..56bc5286 100644 --- a/orion-visor-launch/src/test/java/org/dromara/visor/launch/ReplaceVersion.java +++ b/orion-visor-launch/src/test/java/org/dromara/visor/launch/ReplaceVersion.java @@ -39,9 +39,9 @@ import java.util.function.Function; */ public class ReplaceVersion { - private static final String TARGET_VERSION = "2.3.9"; + private static final String TARGET_VERSION = "2.4.0"; - private static final String REPLACE_VERSION = "2.4.0"; + private static final String REPLACE_VERSION = "2.4.1"; private static final String PATH = new File("").getAbsolutePath(); diff --git a/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/handler/host/extra/model/HostRdpExtraModel.java b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/handler/host/extra/model/HostRdpExtraModel.java index 2f466be1..4b75d1cb 100644 --- a/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/handler/host/extra/model/HostRdpExtraModel.java +++ b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/handler/host/extra/model/HostRdpExtraModel.java @@ -56,4 +56,9 @@ public class HostRdpExtraModel implements GenericsDataModel { */ private Boolean lowBandwidthMode; + /** + * 初始化程序 + */ + private String initialProgram; + } diff --git a/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/service/HostConfigService.java b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/service/HostConfigService.java index 2595ba88..0742ac7a 100644 --- a/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/service/HostConfigService.java +++ b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/service/HostConfigService.java @@ -37,6 +37,14 @@ import java.util.List; */ public interface HostConfigService { + /** + * 初始化主机配置 + * + * @param hostId hostId + * @param types types + */ + void initHostConfig(Long hostId, List types); + /** * 更新主机配置 * diff --git a/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/service/impl/HostConfigServiceImpl.java b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/service/impl/HostConfigServiceImpl.java index fba78e3c..96414948 100644 --- a/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/service/impl/HostConfigServiceImpl.java +++ b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/service/impl/HostConfigServiceImpl.java @@ -67,6 +67,34 @@ public class HostConfigServiceImpl implements HostConfigService { @Resource private HostConfigDAO hostConfigDAO; + @Override + public void initHostConfig(Long hostId, List types) { + // 查询主机配置类型 + List hostConfigTypes = hostConfigDAO.selectByHostId(hostId) + .stream() + .map(HostConfigDO::getType) + .collect(Collectors.toList()); + List configs = new ArrayList<>(); + for (String type : types) { + // 配置存在则跳过 + if (hostConfigTypes.contains(type)) { + continue; + } + // 配置不存在则初始化 + HostConfigDO config = HostConfigDO.builder() + .hostId(hostId) + .type(type) + .status(EnableStatus.ENABLED.name()) + .config(HostConfigStrategyEnum.of(type).getDefault().serial()) + .build(); + configs.add(config); + } + // 插入主机配置 + if (!configs.isEmpty()) { + hostConfigDAO.insertBatch(configs); + } + } + @Override public Integer updateHostConfig(HostConfigUpdateRequest request) { log.info("HostConfigService-updateHostConfig request: {}", request); diff --git a/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/service/impl/HostConnectServiceImpl.java b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/service/impl/HostConnectServiceImpl.java index 085e44b1..93240932 100644 --- a/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/service/impl/HostConnectServiceImpl.java +++ b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/service/impl/HostConnectServiceImpl.java @@ -293,8 +293,9 @@ public class HostConnectServiceImpl implements HostConnectService { // 填充基础主机信息 this.setBaseConnectConfig(connectConfig, host); if (extra != null) { - // 设置低带宽模式 + // 设置额外配置信息 connectConfig.setLowBandwidthMode(extra.getLowBandwidthMode()); + connectConfig.setInitialProgram(extra.getInitialProgram()); // 获取自定义认证方式 HostExtraAuthTypeEnum extraAuthType = HostExtraAuthTypeEnum.of(extra.getAuthType()); if (HostExtraAuthTypeEnum.CUSTOM_IDENTITY.equals(extraAuthType)) { diff --git a/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/service/impl/HostServiceImpl.java b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/service/impl/HostServiceImpl.java index 94e33f98..b5a2c178 100644 --- a/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/service/impl/HostServiceImpl.java +++ b/orion-visor-modules/orion-visor-module-asset/orion-visor-module-asset-service/src/main/java/org/dromara/visor/module/asset/service/impl/HostServiceImpl.java @@ -128,8 +128,10 @@ public class HostServiceImpl implements HostService { this.checkHostCodePresent(record); // 插入主机 int effect = hostDAO.insert(record); - log.info("HostService-createHost effect: {}", effect); Long id = record.getId(); + log.info("HostService-createHost id: {}, effect: {}", id, effect); + // 初始化主机配置 + hostConfigService.initHostConfig(id, request.getTypes()); // 插入 tag tagRelApi.addTagRel(TagTypeEnum.HOST, id, request.getTags()); // 引用分组 @@ -183,6 +185,8 @@ public class HostServiceImpl implements HostService { // 修改 config 状态 hostConfigDAO.updateConfigStatus(id, types, EnableStatus.ENABLED.name()); hostConfigDAO.updateConfigInvertStatus(id, types, EnableStatus.DISABLED.name()); + // 初始化主机配置 + hostConfigService.initHostConfig(id, types); // 删除缓存 this.clearCache(); return effect; diff --git a/orion-visor-modules/orion-visor-module-common/src/main/java/org/dromara/visor/module/common/utils/SftpUtils.java b/orion-visor-modules/orion-visor-module-common/src/main/java/org/dromara/visor/module/common/utils/SftpUtils.java index 2c662e1f..6dda7b46 100644 --- a/orion-visor-modules/orion-visor-module-common/src/main/java/org/dromara/visor/module/common/utils/SftpUtils.java +++ b/orion-visor-modules/orion-visor-module-common/src/main/java/org/dromara/visor/module/common/utils/SftpUtils.java @@ -22,10 +22,8 @@ */ package org.dromara.visor.module.common.utils; -import cn.orionsec.kit.lang.constant.Letters; import cn.orionsec.kit.lang.utils.Booleans; import cn.orionsec.kit.lang.utils.Strings; -import cn.orionsec.kit.lang.utils.io.Files1; import cn.orionsec.kit.net.host.sftp.SftpExecutor; import cn.orionsec.kit.net.host.sftp.SftpFile; import cn.orionsec.kit.spring.SpringHolder; @@ -69,21 +67,4 @@ public class SftpUtils { } } - /** - * 获取移动目标路径 - * - * @param source source - * @param target target - * @return absolute target - */ - public static String getAbsoluteTargetPath(String source, String target) { - if (target.charAt(0) == Letters.SLASH) { - // 绝对路径 - return Files1.getPath(Files1.normalize(target)); - } else { - // 相对路径 - return Files1.getPath(Files1.normalize(Files1.getPath(source + "/../" + target))); - } - } - } diff --git a/orion-visor-modules/orion-visor-module-infra/orion-visor-module-infra-service/src/main/java/org/dromara/visor/module/infra/controller/AuthenticationController.java b/orion-visor-modules/orion-visor-module-infra/orion-visor-module-infra-service/src/main/java/org/dromara/visor/module/infra/controller/AuthenticationController.java index 2f219af2..4c1ab5ec 100644 --- a/orion-visor-modules/orion-visor-module-infra/orion-visor-module-infra-service/src/main/java/org/dromara/visor/module/infra/controller/AuthenticationController.java +++ b/orion-visor-modules/orion-visor-module-infra/orion-visor-module-infra-service/src/main/java/org/dromara/visor/module/infra/controller/AuthenticationController.java @@ -26,6 +26,7 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.extern.slf4j.Slf4j; import org.dromara.visor.framework.biz.operator.log.core.annotation.OperatorLog; +import org.dromara.visor.framework.biz.operator.log.core.enums.ReturnType; import org.dromara.visor.framework.log.core.annotation.IgnoreLog; import org.dromara.visor.framework.log.core.enums.IgnoreLogMode; import org.dromara.visor.framework.web.core.annotation.RestWrapper; @@ -58,7 +59,7 @@ public class AuthenticationController { @Resource private AuthenticationService authenticationService; - @OperatorLog(AuthenticationOperatorType.LOGIN) + @OperatorLog(value = AuthenticationOperatorType.LOGIN, ret = ReturnType.IGNORE) @PermitAll @Operation(summary = "登录") @PostMapping("/login") diff --git a/orion-visor-modules/orion-visor-module-infra/orion-visor-module-infra-service/src/main/java/org/dromara/visor/module/infra/controller/UserAggregateController.java b/orion-visor-modules/orion-visor-module-infra/orion-visor-module-infra-service/src/main/java/org/dromara/visor/module/infra/controller/UserAggregateController.java index 85c1dc34..c43678d2 100644 --- a/orion-visor-modules/orion-visor-module-infra/orion-visor-module-infra-service/src/main/java/org/dromara/visor/module/infra/controller/UserAggregateController.java +++ b/orion-visor-modules/orion-visor-module-infra/orion-visor-module-infra-service/src/main/java/org/dromara/visor/module/infra/controller/UserAggregateController.java @@ -25,6 +25,8 @@ package org.dromara.visor.module.infra.controller; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.extern.slf4j.Slf4j; +import org.dromara.visor.common.constant.AppConst; +import org.dromara.visor.common.constant.HttpHeaderConst; import org.dromara.visor.framework.log.core.annotation.IgnoreLog; import org.dromara.visor.framework.log.core.enums.IgnoreLogMode; import org.dromara.visor.framework.web.core.annotation.RestWrapper; @@ -37,6 +39,7 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; import java.util.List; /** @@ -67,7 +70,12 @@ public class UserAggregateController { @IgnoreLog(IgnoreLogMode.RET) @GetMapping("/user") @Operation(summary = "获取用户权限聚合信息") - public UserAggregateVO getUserAggregateInfo() { + public UserAggregateVO getUserAggregateInfo(HttpServletResponse response) { + // FIXME KIT + // 设置版本号请求头 + response.setHeader(HttpHeaderConst.ACCESS_CONTROL_EXPOSE_HEADERS, HttpHeaderConst.APP_VERSION); + response.setHeader(HttpHeaderConst.APP_VERSION, AppConst.VERSION); + // 获取用户信息 return userAggregateService.getUserAggregateInfo(); } diff --git a/orion-visor-modules/orion-visor-module-infra/orion-visor-module-infra-service/src/main/java/org/dromara/visor/module/infra/define/cache/PreferenceCacheKeyDefine.java b/orion-visor-modules/orion-visor-module-infra/orion-visor-module-infra-service/src/main/java/org/dromara/visor/module/infra/define/cache/PreferenceCacheKeyDefine.java index 5f6a1d89..2075a57d 100644 --- a/orion-visor-modules/orion-visor-module-infra/orion-visor-module-infra-service/src/main/java/org/dromara/visor/module/infra/define/cache/PreferenceCacheKeyDefine.java +++ b/orion-visor-modules/orion-visor-module-infra/orion-visor-module-infra-service/src/main/java/org/dromara/visor/module/infra/define/cache/PreferenceCacheKeyDefine.java @@ -39,7 +39,7 @@ import java.util.concurrent.TimeUnit; public interface PreferenceCacheKeyDefine { CacheKeyDefine PREFERENCE = new CacheKeyBuilder() - .key("user:prefer:{}:{}") + .key("v1:user:prefer:{}:{}") .desc("用户偏好 ${userId} ${type}") .type(JSONObject.class) .struct(RedisCacheStruct.STRING) diff --git a/orion-visor-modules/orion-visor-module-infra/orion-visor-module-infra-service/src/main/java/org/dromara/visor/module/infra/entity/vo/AppInfoVO.java b/orion-visor-modules/orion-visor-module-infra/orion-visor-module-infra-service/src/main/java/org/dromara/visor/module/infra/entity/vo/AppInfoVO.java index 86ce5cb3..cb6c115e 100644 --- a/orion-visor-modules/orion-visor-module-infra/orion-visor-module-infra-service/src/main/java/org/dromara/visor/module/infra/entity/vo/AppInfoVO.java +++ b/orion-visor-modules/orion-visor-module-infra/orion-visor-module-infra-service/src/main/java/org/dromara/visor/module/infra/entity/vo/AppInfoVO.java @@ -49,7 +49,4 @@ public class AppInfoVO implements Serializable { @Schema(description = "系统版本") private String version; - @Schema(description = "机器码") - private String uuid; - } diff --git a/orion-visor-modules/orion-visor-module-infra/orion-visor-module-infra-service/src/main/java/org/dromara/visor/module/infra/handler/preference/model/TerminalPreferenceModel.java b/orion-visor-modules/orion-visor-module-infra/orion-visor-module-infra-service/src/main/java/org/dromara/visor/module/infra/handler/preference/model/TerminalPreferenceModel.java index 6e9eb68d..b57614ca 100644 --- a/orion-visor-modules/orion-visor-module-infra/orion-visor-module-infra-service/src/main/java/org/dromara/visor/module/infra/handler/preference/model/TerminalPreferenceModel.java +++ b/orion-visor-modules/orion-visor-module-infra/orion-visor-module-infra-service/src/main/java/org/dromara/visor/module/infra/handler/preference/model/TerminalPreferenceModel.java @@ -51,9 +51,9 @@ public class TerminalPreferenceModel implements GenericsDataModel { private String newConnectionType; /** - * 终端主题 + * ssh 主题 */ - private JSONObject theme; + private JSONObject sshTheme; /** * ssh 显示设置 @@ -61,40 +61,40 @@ public class TerminalPreferenceModel implements GenericsDataModel { private JSONObject sshDisplaySetting; /** - * rdp 图形化设置 + * ssh 右键菜单设置 */ - private JSONObject rdpGraphSetting; + private List sshRightMenuSetting; /** * ssh 操作栏设置 */ private JSONObject sshActionBarSetting; + /** + * ssh 交互设置 + */ + private JSONObject sshInteractSetting; + + /** + * ssh 插件设置 + */ + private JSONObject sshPluginsSetting; + + /** + * rdp 会话设置 + */ + private JSONObject rdpSessionSetting; + + /** + * rdp 图形化设置 + */ + private JSONObject rdpGraphSetting; + /** * rdp 操作栏设置 */ private JSONObject rdpActionBarSetting; - /** - * 右键菜单设置 - */ - private List rightMenuSetting; - - /** - * 交互设置 - */ - private JSONObject interactSetting; - - /** - * 插件设置 - */ - private JSONObject pluginsSetting; - - /** - * 会话设置 - */ - private JSONObject sessionSetting; - /** * 快捷键设置 */ @@ -148,94 +148,6 @@ public class TerminalPreferenceModel implements GenericsDataModel { } - @Data - @Builder - @NoArgsConstructor - @AllArgsConstructor - public static class RdpGraphSettingModel implements IJsonObject { - - /** - * 显示大小 - */ - private String displaySize; - - /** - * 显示宽度 - */ - private Integer displayWidth; - - /** - * 显示高度 - */ - private Integer displayHeight; - - /** - * 启用音频输入 - */ - private Boolean enableAudioInput; - - /** - * 启用音频输出 - */ - private Boolean enableAudioOutput; - - /** - * 颜色深度 - */ - private Integer colorDepth; - - /** - * 无损压缩 - */ - private Boolean forceLossless; - - /** - * 启用壁纸 - */ - private Boolean enableWallpaper; - - /** - * 启用主题 - */ - private Boolean enableTheming; - - /** - * 启动平滑字体 - */ - private Boolean enableFontSmoothing; - - /** - * 启用窗口拖动 - */ - private Boolean enableFullWindowDrag; - - /** - * 启用桌面合成 - */ - private Boolean enableDesktopComposition; - - /** - * 启用菜单动画 - */ - private Boolean enableMenuAnimations; - - /** - * 禁用位图缓存 - */ - private Boolean disableBitmapCaching; - - /** - * 禁用离屏缓存 - */ - private Boolean disableOffscreenCaching; - - /** - * 禁用字形缓存 - */ - private Boolean disableGlyphCaching; - - } - @Data @Builder @NoArgsConstructor @@ -323,55 +235,7 @@ public class TerminalPreferenceModel implements GenericsDataModel { @Builder @NoArgsConstructor @AllArgsConstructor - public static class RdpActionBarSettingModel implements IJsonObject { - - /** - * 位置 - */ - private String position; - - /** - * 显示设置 - */ - private Boolean display; - - /** - * 组合键 - */ - private Boolean combinationKey; - - /** - * 剪切板 - */ - private Boolean clipboard; - - /** - * 上传 - */ - private Boolean upload; - - /** - * 保存为 rdp 文件 - */ - private Boolean saveRdp; - - /** - * 断开连接 - */ - private Boolean disconnect; - - /** - * 关闭 - */ - private Boolean close; - - } - - @Data - @Builder - @NoArgsConstructor - @AllArgsConstructor - public static class InteractSettingModel implements IJsonObject { + public static class SshInteractSettingModel implements IJsonObject { /** * 快速滚动 @@ -423,13 +287,23 @@ public class TerminalPreferenceModel implements GenericsDataModel { */ private String wordSeparator; + /** + * 伪终端类型 + */ + private String terminalEmulationType; + + /** + * 保存在缓冲区的行数 + */ + private Integer scrollBackLine; + } @Data @Builder @NoArgsConstructor @AllArgsConstructor - public static class PluginsSettingModel implements IJsonObject { + public static class SshPluginsSettingModel implements IJsonObject { /** * 超链接插件 @@ -457,17 +331,153 @@ public class TerminalPreferenceModel implements GenericsDataModel { @Builder @NoArgsConstructor @AllArgsConstructor - public static class SessionSettingModel implements IJsonObject { + public static class RdpGraphSettingModel implements IJsonObject { /** - * 伪终端类型 + * 显示大小 */ - private String terminalEmulationType; + private String displaySize; /** - * 保存在缓冲区的行数 + * 显示宽度 */ - private Integer scrollBackLine; + private Integer displayWidth; + + /** + * 显示高度 + */ + private Integer displayHeight; + + /** + * 颜色深度 + */ + private Integer colorDepth; + + /** + * 无损压缩 + */ + private Boolean forceLossless; + + /** + * 启用壁纸 + */ + private Boolean enableWallpaper; + + /** + * 启用主题 + */ + private Boolean enableTheming; + + /** + * 启动平滑字体 + */ + private Boolean enableFontSmoothing; + + /** + * 启用窗口拖动 + */ + private Boolean enableFullWindowDrag; + + /** + * 启用桌面合成 + */ + private Boolean enableDesktopComposition; + + /** + * 启用菜单动画 + */ + private Boolean enableMenuAnimations; + + /** + * 禁用位图缓存 + */ + private Boolean disableBitmapCaching; + + /** + * 禁用离屏缓存 + */ + private Boolean disableOffscreenCaching; + + /** + * 禁用字形缓存 + */ + private Boolean disableGlyphCaching; + + /** + * 禁用图形加速 + */ + private Boolean disableGfx; + + } + + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class RdpActionBarSettingModel implements IJsonObject { + + /** + * 位置 + */ + private String position; + + /** + * 显示设置 + */ + private Boolean display; + + /** + * 组合键 + */ + private Boolean combinationKey; + + /** + * 剪切板 + */ + private Boolean clipboard; + + /** + * 上传 + */ + private Boolean upload; + + /** + * 保存为 rdp 文件 + */ + private Boolean saveRdp; + + /** + * 断开连接 + */ + private Boolean disconnect; + + /** + * 关闭 + */ + private Boolean close; + + } + + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class RdpSessionSettingModel implements IJsonObject { + + /** + * 启用音频输入 + */ + private Boolean enableAudioInput; + + /** + * 启用音频输出 + */ + private Boolean enableAudioOutput; + + /** + * 驱动挂载模式 + */ + private String driveMountMode; } diff --git a/orion-visor-modules/orion-visor-module-infra/orion-visor-module-infra-service/src/main/java/org/dromara/visor/module/infra/handler/preference/strategy/TerminalPreferenceStrategy.java b/orion-visor-modules/orion-visor-module-infra/orion-visor-module-infra-service/src/main/java/org/dromara/visor/module/infra/handler/preference/strategy/TerminalPreferenceStrategy.java index 6bf4a98b..68458b26 100644 --- a/orion-visor-modules/orion-visor-module-infra/orion-visor-module-infra-service/src/main/java/org/dromara/visor/module/infra/handler/preference/strategy/TerminalPreferenceStrategy.java +++ b/orion-visor-modules/orion-visor-module-infra/orion-visor-module-infra-service/src/main/java/org/dromara/visor/module/infra/handler/preference/strategy/TerminalPreferenceStrategy.java @@ -51,23 +51,23 @@ public class TerminalPreferenceStrategy extends AbstractGenericsDataStrategy getDefaultSshRightMenuSetting() { + return Lists.of("selectAll", "copy", "paste", "search", "clear"); } /** @@ -147,6 +129,70 @@ public class TerminalPreferenceStrategy extends AbstractGenericsDataStrategy~!@#$%^&*|+=[]{}~?│") + .terminalEmulationType(TerminalType.XTERM.getType()) + .scrollBackLine(1000) + .build() + .toJsonString(); + } + + /** + * 获取默认插件设置 + * + * @return setting + */ + private String getDefaultSshPluginsSetting() { + return TerminalPreferenceModel.SshPluginsSettingModel.builder() + .enableWeblinkPlugin(true) + .enableWebglPlugin(true) + .enableUnicodePlugin(true) + .enableImagePlugin(false) + .build() + .toJsonString(); + } + + /** + * 获取 rdp 图形化默认设置 + * + * @return setting + */ + private String getDefaultRdpGraphSetting() { + return TerminalPreferenceModel.RdpGraphSettingModel.builder() + .displaySize("fit") + .displayWidth(0) + .displayHeight(0) + .colorDepth(24) + .forceLossless(true) + .enableWallpaper(true) + .enableTheming(true) + .enableFontSmoothing(true) + .enableFullWindowDrag(true) + .enableDesktopComposition(true) + .enableMenuAnimations(false) + .disableBitmapCaching(false) + .disableOffscreenCaching(false) + .disableGlyphCaching(false) + .disableGfx(false) + .build() + .toJsonString(); + } + /** * 获取 rdp 操作栏默认设置 * @@ -167,59 +213,15 @@ public class TerminalPreferenceStrategy extends AbstractGenericsDataStrategy getDefaultRightMenuSetting() { - return Lists.of("selectAll", "copy", "paste", "search", "clear"); - } - - /** - * 获取默认交互设置 - * - * @return setting - */ - private String getDefaultInteractSetting() { - return TerminalPreferenceModel.InteractSettingModel.builder() - .fastScrollModifier(true) - .altClickMovesCursor(true) - .rightClickSelectsWord(false) - .selectionChangeCopy(false) - .copyAutoTrim(false) - .pasteAutoTrim(false) - .rightClickPaste(false) - .enableRightClickMenu(true) - .enableBell(false) - .wordSeparator("/\\()\"'` -.,:;<>~!@#$%^&*|+=[]{}~?│") - .build() - .toJsonString(); - } - - /** - * 获取默认插件设置 - * - * @return setting - */ - private String getDefaultPluginsSetting() { - return TerminalPreferenceModel.PluginsSettingModel.builder() - .enableWeblinkPlugin(true) - .enableWebglPlugin(true) - .enableUnicodePlugin(true) - .enableImagePlugin(false) - .build() - .toJsonString(); - } - - /** - * 获取默认会话设置 - * - * @return setting - */ - private String getDefaultSessionSetting() { - return TerminalPreferenceModel.SessionSettingModel.builder() - .terminalEmulationType(TerminalType.XTERM.getType()) - .scrollBackLine(1000) + private String getDefaultRdpSessionSetting() { + return TerminalPreferenceModel.RdpSessionSettingModel.builder() + .enableAudioInput(false) + .enableAudioOutput(true) + .driveMountMode("ASSET") .build() .toJsonString(); } diff --git a/orion-visor-modules/orion-visor-module-infra/orion-visor-module-infra-service/src/main/java/org/dromara/visor/module/infra/service/impl/SystemSettingServiceImpl.java b/orion-visor-modules/orion-visor-module-infra/orion-visor-module-infra-service/src/main/java/org/dromara/visor/module/infra/service/impl/SystemSettingServiceImpl.java index 8c225d25..e40d0e5c 100644 --- a/orion-visor-modules/orion-visor-module-infra/orion-visor-module-infra-service/src/main/java/org/dromara/visor/module/infra/service/impl/SystemSettingServiceImpl.java +++ b/orion-visor-modules/orion-visor-module-infra/orion-visor-module-infra-service/src/main/java/org/dromara/visor/module/infra/service/impl/SystemSettingServiceImpl.java @@ -45,7 +45,6 @@ import org.dromara.visor.module.infra.entity.request.system.SystemSettingUpdateR import org.dromara.visor.module.infra.entity.vo.AppInfoVO; import org.dromara.visor.module.infra.entity.vo.RsaKeyPairVO; import org.dromara.visor.module.infra.service.SystemSettingService; -import org.dromara.visor.module.infra.utils.SystemUuidUtils; import org.springframework.stereotype.Service; import javax.annotation.Resource; @@ -73,7 +72,6 @@ public class SystemSettingServiceImpl implements SystemSettingService { public AppInfoVO getAppInfo() { return AppInfoVO.builder() .version(AppConst.VERSION) - .uuid(SystemUuidUtils.getSystemUuid()) .build(); } diff --git a/orion-visor-modules/orion-visor-module-infra/orion-visor-module-infra-service/src/main/java/org/dromara/visor/module/infra/utils/SystemUuidUtils.java b/orion-visor-modules/orion-visor-module-infra/orion-visor-module-infra-service/src/main/java/org/dromara/visor/module/infra/utils/SystemUuidUtils.java deleted file mode 100644 index 2876044f..00000000 --- a/orion-visor-modules/orion-visor-module-infra/orion-visor-module-infra-service/src/main/java/org/dromara/visor/module/infra/utils/SystemUuidUtils.java +++ /dev/null @@ -1,114 +0,0 @@ -/* - * Copyright (c) 2023 - present Dromara, All rights reserved. - * - * https://visor.dromara.org - * https://visor.dromara.org.cn - * https://visor.orionsec.cn - * - * Members: - * Jiahang Li - ljh1553488six@139.com - author - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.dromara.visor.module.infra.utils; - -import cn.orionsec.kit.ext.process.ProcessAwaitExecutor; -import cn.orionsec.kit.lang.support.Attempt; -import cn.orionsec.kit.lang.utils.Arrays1; -import cn.orionsec.kit.lang.utils.Strings; -import cn.orionsec.kit.lang.utils.crypto.Signatures; -import cn.orionsec.kit.lang.utils.io.Streams; -import org.dromara.visor.common.constant.Const; - -import java.io.ByteArrayOutputStream; - -/** - * 系统 UUID 工具类 - * - * @author Jiahang Li - * @version 1.0.0 - * @since 2025/1/16 11:07 - */ -public class SystemUuidUtils { - - private static String uuid; - - private SystemUuidUtils() { - } - - /** - * 获取系统 uuid - * - * @return uuid - */ - public static String getSystemUuid() { - if (SystemUuidUtils.uuid != null) { - return SystemUuidUtils.uuid; - } - String[][] cmd = new String[][]{ - new String[]{"/bin/sh", "-c", "cat /sys/class/dmi/id/product_serial"}, - new String[]{"/bin/bash", "-c", "cat /sys/class/dmi/id/product_serial"}, - new String[]{"/bin/sh", "-c", "dmidecode -s system-uuid"}, - new String[]{"/bin/bash", "-c", "dmidecode -s system-uuid"}, - new String[]{"cmd", "/c", "wmic csproduct get uuid"} - }; - for (String[] s : cmd) { - try { - String uuid = SystemUuidUtils.getCommandOutput(s); - if (Strings.isBlank(uuid)) { - continue; - } - // 去除符号并且转为大写 - uuid = uuid.replaceAll(Const.DASHED, Const.EMPTY) - .toUpperCase() - .trim(); - // 去除 \n - String extraUuid = Arrays1.last(uuid.trim().split(Const.LF)); - if (!Strings.isBlank(extraUuid)) { - uuid = extraUuid.trim(); - } - // 去除 : - extraUuid = Arrays1.last(uuid.trim().split(Const.COLON)); - if (!Strings.isBlank(extraUuid)) { - uuid = extraUuid.trim(); - } - return SystemUuidUtils.uuid = Signatures.md5(uuid); - } catch (Exception e) { - // IGNORED - } - } - return SystemUuidUtils.uuid = Const.UNKNOWN; - } - - /** - * 获取输出结果 - * - * @param command command - * @return result - */ - public static String getCommandOutput(String[] command) { - ByteArrayOutputStream out = new ByteArrayOutputStream(); - ProcessAwaitExecutor executor = new ProcessAwaitExecutor(command); - try { - executor.streamHandler(i -> Attempt.uncheck(Streams::transfer, i, out)) - .waitFor() - .sync() - .exec(); - return out.toString(); - } finally { - Streams.close(out); - Streams.close(executor); - } - } - -} diff --git a/orion-visor-modules/orion-visor-module-terminal/orion-visor-module-terminal-service/src/main/java/org/dromara/visor/module/terminal/controller/TerminalFileLogController.java b/orion-visor-modules/orion-visor-module-terminal/orion-visor-module-terminal-service/src/main/java/org/dromara/visor/module/terminal/controller/TerminalFileLogController.java new file mode 100644 index 00000000..8e9b2a81 --- /dev/null +++ b/orion-visor-modules/orion-visor-module-terminal/orion-visor-module-terminal-service/src/main/java/org/dromara/visor/module/terminal/controller/TerminalFileLogController.java @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2023 - present Dromara, All rights reserved. + * + * https://visor.dromara.org + * https://visor.dromara.org.cn + * https://visor.orionsec.cn + * + * Members: + * Jiahang Li - ljh1553488six@139.com - author + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dromara.visor.module.terminal.controller; + +import cn.orionsec.kit.lang.define.wrapper.DataGrid; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.slf4j.Slf4j; +import org.dromara.visor.common.validator.group.Page; +import org.dromara.visor.framework.biz.operator.log.core.annotation.OperatorLog; +import org.dromara.visor.framework.log.core.annotation.IgnoreLog; +import org.dromara.visor.framework.log.core.enums.IgnoreLogMode; +import org.dromara.visor.framework.web.core.annotation.RestWrapper; +import org.dromara.visor.module.terminal.define.operator.TerminalFileLogOperatorType; +import org.dromara.visor.module.terminal.entity.request.terminal.TerminalFileLogQueryRequest; +import org.dromara.visor.module.terminal.entity.vo.TerminalFileLogVO; +import org.dromara.visor.module.terminal.service.TerminalFileLogService; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import java.util.List; + +/** + * 终端文件日志操作服务 api + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2023-12-26 22:09 + */ +@Tag(name = "terminal - 终端文件日志操作服务") +@Slf4j +@Validated +@RestWrapper +@RestController +@RequestMapping("/terminal/terminal-file-log") +public class TerminalFileLogController { + + @Resource + private TerminalFileLogService terminalFileLogService; + + @IgnoreLog(IgnoreLogMode.RET) + @PostMapping("/query") + @Operation(summary = "分页查询终端文件操作日志") + @PreAuthorize("@ss.hasAnyPermission('infra:operator-log:query', 'terminal:terminal-file-log:management:query')") + public DataGrid getTerminalFileLogPage(@Validated(Page.class) @RequestBody TerminalFileLogQueryRequest request) { + return terminalFileLogService.getTerminalFileLogPage(request); + } + + @IgnoreLog(IgnoreLogMode.RET) + @PostMapping("/count") + @Operation(summary = "查询终端文件操作日志数量") + @PreAuthorize("@ss.hasAnyPermission('infra:operator-log:query', 'terminal:terminal-file-log:management:query')") + public Long getTerminalFileLogCount(@Validated @RequestBody TerminalFileLogQueryRequest request) { + return terminalFileLogService.getTerminalFileLogCount(request); + } + + @OperatorLog(TerminalFileLogOperatorType.DELETE) + @DeleteMapping("/delete") + @Operation(summary = "删除终端文件操作日志") + @Parameter(name = "idList", description = "idList", required = true) + @PreAuthorize("@ss.hasAnyPermission('infra:operator-log:delete', 'terminal:terminal-file-log:management:delete')") + public Integer deleteTerminalFileLog(@RequestParam("idList") List idList) { + return terminalFileLogService.deleteTerminalFileLog(idList); + } + +} diff --git a/orion-visor-modules/orion-visor-module-terminal/orion-visor-module-terminal-service/src/main/java/org/dromara/visor/module/terminal/controller/TerminalSftpController.java b/orion-visor-modules/orion-visor-module-terminal/orion-visor-module-terminal-service/src/main/java/org/dromara/visor/module/terminal/controller/TerminalSftpController.java index 62386b2e..3cfc4a95 100644 --- a/orion-visor-modules/orion-visor-module-terminal/orion-visor-module-terminal-service/src/main/java/org/dromara/visor/module/terminal/controller/TerminalSftpController.java +++ b/orion-visor-modules/orion-visor-module-terminal/orion-visor-module-terminal-service/src/main/java/org/dromara/visor/module/terminal/controller/TerminalSftpController.java @@ -22,22 +22,15 @@ */ package org.dromara.visor.module.terminal.controller; -import cn.orionsec.kit.lang.define.wrapper.DataGrid; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.extern.slf4j.Slf4j; -import org.dromara.visor.common.validator.group.Page; -import org.dromara.visor.framework.biz.operator.log.core.annotation.OperatorLog; import org.dromara.visor.framework.log.core.annotation.IgnoreLog; import org.dromara.visor.framework.log.core.enums.IgnoreLogMode; import org.dromara.visor.framework.web.core.annotation.IgnoreWrapper; import org.dromara.visor.framework.web.core.annotation.RestWrapper; -import org.dromara.visor.module.terminal.define.operator.TerminalOperatorType; -import org.dromara.visor.module.terminal.entity.request.terminal.TerminalSftpLogQueryRequest; -import org.dromara.visor.module.terminal.entity.vo.TerminalSftpLogVO; import org.dromara.visor.module.terminal.service.TerminalSftpService; -import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; @@ -46,7 +39,6 @@ import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBo import javax.annotation.Resource; import javax.annotation.security.PermitAll; import javax.servlet.http.HttpServletResponse; -import java.util.List; /** * SFTP 操作服务 api @@ -66,31 +58,6 @@ public class TerminalSftpController { @Resource private TerminalSftpService terminalSftpService; - @IgnoreLog(IgnoreLogMode.RET) - @PostMapping("/query-log") - @Operation(summary = "分页查询 SFTP 操作日志") - @PreAuthorize("@ss.hasAnyPermission('infra:operator-log:query', 'terminal:terminal-sftp-log:management:query')") - public DataGrid getTerminalSftpLogPage(@Validated(Page.class) @RequestBody TerminalSftpLogQueryRequest request) { - return terminalSftpService.getTerminalSftpLogPage(request); - } - - @IgnoreLog(IgnoreLogMode.RET) - @PostMapping("/log-count") - @Operation(summary = "查询 SFTP 操作日志数量") - @PreAuthorize("@ss.hasAnyPermission('infra:operator-log:query', 'terminal:terminal-sftp-log:management:query')") - public Long getTerminalSftpLogCount(@Validated @RequestBody TerminalSftpLogQueryRequest request) { - return terminalSftpService.getTerminalSftpLogCount(request); - } - - @OperatorLog(TerminalOperatorType.DELETE_SFTP_LOG) - @DeleteMapping("/delete-log") - @Operation(summary = "删除 SFTP 操作日志") - @Parameter(name = "idList", description = "idList", required = true) - @PreAuthorize("@ss.hasAnyPermission('infra:operator-log:delete', 'terminal:terminal-sftp-log:management:delete')") - public Integer deleteTerminalSftpLog(@RequestParam("idList") List idList) { - return terminalSftpService.deleteTerminalSftpLog(idList); - } - @IgnoreLog(IgnoreLogMode.RET) @GetMapping("/get-content") @Operation(summary = "获取文件内容") diff --git a/orion-visor-modules/orion-visor-module-terminal/orion-visor-module-terminal-service/src/main/java/org/dromara/visor/module/terminal/convert/TerminalFileLogConvert.java b/orion-visor-modules/orion-visor-module-terminal/orion-visor-module-terminal-service/src/main/java/org/dromara/visor/module/terminal/convert/TerminalFileLogConvert.java new file mode 100644 index 00000000..38bae427 --- /dev/null +++ b/orion-visor-modules/orion-visor-module-terminal/orion-visor-module-terminal-service/src/main/java/org/dromara/visor/module/terminal/convert/TerminalFileLogConvert.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2023 - present Dromara, All rights reserved. + * + * https://visor.dromara.org + * https://visor.dromara.org.cn + * https://visor.orionsec.cn + * + * Members: + * Jiahang Li - ljh1553488six@139.com - author + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dromara.visor.module.terminal.convert; + +import org.dromara.visor.module.infra.entity.dto.operator.OperatorLogDTO; +import org.dromara.visor.module.terminal.entity.vo.TerminalFileLogVO; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.factory.Mappers; + +/** + * 终端文件操作日志 内部对象转换器 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2023-12-26 22:09 + */ +@Mapper +public interface TerminalFileLogConvert { + + TerminalFileLogConvert MAPPER = Mappers.getMapper(TerminalFileLogConvert.class); + + @Mapping(target = "extra", ignore = true) + TerminalFileLogVO to(OperatorLogDTO request); + +} diff --git a/orion-visor-modules/orion-visor-module-terminal/orion-visor-module-terminal-service/src/main/java/org/dromara/visor/module/terminal/define/operator/TerminalFileLogOperatorType.java b/orion-visor-modules/orion-visor-module-terminal/orion-visor-module-terminal-service/src/main/java/org/dromara/visor/module/terminal/define/operator/TerminalFileLogOperatorType.java new file mode 100644 index 00000000..005cb09d --- /dev/null +++ b/orion-visor-modules/orion-visor-module-terminal/orion-visor-module-terminal-service/src/main/java/org/dromara/visor/module/terminal/define/operator/TerminalFileLogOperatorType.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2023 - present Dromara, All rights reserved. + * + * https://visor.dromara.org + * https://visor.dromara.org.cn + * https://visor.orionsec.cn + * + * Members: + * Jiahang Li - ljh1553488six@139.com - author + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dromara.visor.module.terminal.define.operator; + +import cn.orionsec.kit.lang.utils.collect.Lists; +import org.dromara.visor.framework.biz.operator.log.core.annotation.Module; +import org.dromara.visor.framework.biz.operator.log.core.enums.OperatorRiskLevel; +import org.dromara.visor.framework.biz.operator.log.core.factory.InitializingOperatorTypes; +import org.dromara.visor.framework.biz.operator.log.core.model.OperatorType; + +import java.util.List; + +/** + * 终端文件日志 操作日志类型 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2024/3/2 14:37 + */ +@Module("terminal:terminal-file-log") +public class TerminalFileLogOperatorType extends InitializingOperatorTypes { + + public static final String DELETE = "terminal-file-log:delete"; + + public static final List TYPES = Lists.of( + TerminalOperatorType.SFTP_MKDIR, + TerminalOperatorType.SFTP_TOUCH, + TerminalOperatorType.SFTP_MOVE, + TerminalOperatorType.SFTP_REMOVE, + TerminalOperatorType.SFTP_TRUNCATE, + TerminalOperatorType.SFTP_CHMOD, + TerminalOperatorType.SFTP_CHOWN, + TerminalOperatorType.SFTP_CHGRP, + TerminalOperatorType.SFTP_GET_CONTENT, + TerminalOperatorType.SFTP_SET_CONTENT, + TerminalOperatorType.SFTP_UPLOAD, + TerminalOperatorType.SFTP_DOWNLOAD, + TerminalOperatorType.RDP_UPLOAD, + TerminalOperatorType.RDP_DOWNLOAD + ); + + @Override + public OperatorType[] types() { + return new OperatorType[]{ + new OperatorType(OperatorRiskLevel.H, DELETE, "删除文件操作日志 ${count} 条"), + }; + } + +} diff --git a/orion-visor-modules/orion-visor-module-terminal/orion-visor-module-terminal-service/src/main/java/org/dromara/visor/module/terminal/define/operator/TerminalOperatorType.java b/orion-visor-modules/orion-visor-module-terminal/orion-visor-module-terminal-service/src/main/java/org/dromara/visor/module/terminal/define/operator/TerminalOperatorType.java index 4535b49f..9752e2f5 100644 --- a/orion-visor-modules/orion-visor-module-terminal/orion-visor-module-terminal-service/src/main/java/org/dromara/visor/module/terminal/define/operator/TerminalOperatorType.java +++ b/orion-visor-modules/orion-visor-module-terminal/orion-visor-module-terminal-service/src/main/java/org/dromara/visor/module/terminal/define/operator/TerminalOperatorType.java @@ -22,14 +22,11 @@ */ package org.dromara.visor.module.terminal.define.operator; -import cn.orionsec.kit.lang.utils.collect.Lists; import org.dromara.visor.framework.biz.operator.log.core.annotation.Module; import org.dromara.visor.framework.biz.operator.log.core.enums.OperatorRiskLevel; import org.dromara.visor.framework.biz.operator.log.core.factory.InitializingOperatorTypes; import org.dromara.visor.framework.biz.operator.log.core.model.OperatorType; -import java.util.List; - /** * 终端 操作日志类型 * @@ -42,8 +39,6 @@ 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"; @@ -68,38 +63,28 @@ public class TerminalOperatorType extends InitializingOperatorTypes { 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_CHOWN, - SFTP_CHGRP, - SFTP_GET_CONTENT, - SFTP_SET_CONTENT, - SFTP_UPLOAD, - SFTP_DOWNLOAD - ); + public static final String RDP_UPLOAD = "terminal:rdp-upload"; + + public static final String RDP_DOWNLOAD = "terminal:rdp-download"; @Override public OperatorType[] types() { return new OperatorType[]{ new OperatorType(OperatorRiskLevel.L, CONNECT, "连接主机 ${connectType} ${hostName}"), - new OperatorType(OperatorRiskLevel.H, DELETE_SFTP_LOG, "删除 SFTP 操作日志 ${count} 条"), new OperatorType(OperatorRiskLevel.L, SFTP_MKDIR, "创建文件夹 ${hostName} ${path}"), new OperatorType(OperatorRiskLevel.L, SFTP_TOUCH, "创建文件 ${hostName} ${path}"), new OperatorType(OperatorRiskLevel.M, SFTP_MOVE, "移动文件 ${hostName} ${path}${target}"), - new OperatorType(OperatorRiskLevel.H, SFTP_REMOVE, "删除文件 ${hostName} ${path}"), + new OperatorType(OperatorRiskLevel.H, SFTP_REMOVE, "删除文件 ${hostName} ${count}个\n${path}"), new OperatorType(OperatorRiskLevel.H, SFTP_TRUNCATE, "截断文件 ${hostName} ${path}"), new OperatorType(OperatorRiskLevel.M, SFTP_CHMOD, "文件提权 ${hostName} ${path} ${mod}"), new OperatorType(OperatorRiskLevel.M, SFTP_CHOWN, "修改文件归属 ${hostName} ${path} ${id}"), new OperatorType(OperatorRiskLevel.M, SFTP_CHGRP, "修改文件分组 ${hostName} ${path} ${id}"), new OperatorType(OperatorRiskLevel.L, SFTP_GET_CONTENT, "获取文件内容 ${hostName} ${path}"), new OperatorType(OperatorRiskLevel.M, SFTP_SET_CONTENT, "修改文件内容 ${hostName} ${path}"), - new OperatorType(OperatorRiskLevel.M, SFTP_UPLOAD, "上传文件 ${hostName} ${path}"), - new OperatorType(OperatorRiskLevel.M, SFTP_DOWNLOAD, "下载文件 ${hostName} ${path}"), + new OperatorType(OperatorRiskLevel.M, SFTP_UPLOAD, "上传文件 ${hostName} (${count}个)\n${path}"), + new OperatorType(OperatorRiskLevel.M, SFTP_DOWNLOAD, "下载文件 ${hostName} (${count}个)\n${path}"), + new OperatorType(OperatorRiskLevel.M, RDP_UPLOAD, "上传文件 ${hostName} ${path}"), + new OperatorType(OperatorRiskLevel.M, RDP_DOWNLOAD, "下载文件 ${hostName} ${path}"), }; } diff --git a/orion-visor-modules/orion-visor-module-terminal/orion-visor-module-terminal-service/src/main/java/org/dromara/visor/module/terminal/entity/request/terminal/TerminalFileLogQueryRequest.java b/orion-visor-modules/orion-visor-module-terminal/orion-visor-module-terminal-service/src/main/java/org/dromara/visor/module/terminal/entity/request/terminal/TerminalFileLogQueryRequest.java new file mode 100644 index 00000000..cf8aeb14 --- /dev/null +++ b/orion-visor-modules/orion-visor-module-terminal/orion-visor-module-terminal-service/src/main/java/org/dromara/visor/module/terminal/entity/request/terminal/TerminalFileLogQueryRequest.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2023 - present Dromara, All rights reserved. + * + * https://visor.dromara.org + * https://visor.dromara.org.cn + * https://visor.orionsec.cn + * + * Members: + * Jiahang Li - ljh1553488six@139.com - author + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dromara.visor.module.terminal.entity.request.terminal; + +import com.fasterxml.jackson.annotation.JsonFormat; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; +import org.dromara.visor.common.entity.BaseQueryRequest; + +import javax.validation.constraints.Size; +import java.util.Date; + +/** + * 终端文件操作日志 查询请求对象 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2024-3-4 22:59 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) +@Schema(name = "TerminalFileLogQueryRequest", description = "终端文件操作日志 查询请求对象") +public class TerminalFileLogQueryRequest extends BaseQueryRequest { + + @Schema(description = "用户id") + private Long userId; + + @Schema(description = "hostId") + private Long hostId; + + @Size(max = 64) + @Schema(description = "操作类型") + private String type; + + @Schema(description = "操作结果 0失败 1成功") + private Integer result; + + @Schema(description = "开始时间-区间") + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") + private Date[] startTimeRange; + +} diff --git a/orion-visor-modules/orion-visor-module-terminal/orion-visor-module-terminal-service/src/main/java/org/dromara/visor/module/terminal/entity/vo/TerminalFileLogVO.java b/orion-visor-modules/orion-visor-module-terminal/orion-visor-module-terminal-service/src/main/java/org/dromara/visor/module/terminal/entity/vo/TerminalFileLogVO.java new file mode 100644 index 00000000..1d640622 --- /dev/null +++ b/orion-visor-modules/orion-visor-module-terminal/orion-visor-module-terminal-service/src/main/java/org/dromara/visor/module/terminal/entity/vo/TerminalFileLogVO.java @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2023 - present Dromara, All rights reserved. + * + * https://visor.dromara.org + * https://visor.dromara.org.cn + * https://visor.orionsec.cn + * + * Members: + * Jiahang Li - ljh1553488six@139.com - author + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dromara.visor.module.terminal.entity.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; +import java.util.Map; + +/** + * 终端文件操作日志 实体对象 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2023-10-10 17:08 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Schema(name = "TerminalFileLogVO", description = "终端文件操作日志 实体对象") +public class TerminalFileLogVO 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[] paths; + + @Schema(description = "请求ip") + private String address; + + @Schema(description = "请求地址") + private String location; + + @Schema(description = "userAgent") + private String userAgent; + + @Schema(description = "操作类型") + private String type; + + @Schema(description = "参数") + private Map extra; + + @Schema(description = "操作结果 0失败 1成功") + private Integer result; + + @Schema(description = "开始时间") + private Date startTime; + +} diff --git a/orion-visor-modules/orion-visor-module-terminal/orion-visor-module-terminal-service/src/main/java/org/dromara/visor/module/terminal/enums/DriveMountModeEnum.java b/orion-visor-modules/orion-visor-module-terminal/orion-visor-module-terminal-service/src/main/java/org/dromara/visor/module/terminal/enums/DriveMountModeEnum.java new file mode 100644 index 00000000..7f9c745f --- /dev/null +++ b/orion-visor-modules/orion-visor-module-terminal/orion-visor-module-terminal-service/src/main/java/org/dromara/visor/module/terminal/enums/DriveMountModeEnum.java @@ -0,0 +1,161 @@ +/* + * Copyright (c) 2023 - present Dromara, All rights reserved. + * + * https://visor.dromara.org + * https://visor.dromara.org.cn + * https://visor.orionsec.cn + * + * Members: + * Jiahang Li - ljh1553488six@139.com - author + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dromara.visor.module.terminal.enums; + +import cn.orionsec.kit.lang.utils.time.Dates; +import lombok.AllArgsConstructor; + +/** + * 驱动挂载模式 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2025/6/29 1:32 + */ +@AllArgsConstructor +public enum DriveMountModeEnum { + + /** + * 完全共享 + */ + SHARED("S") { + @Override + public String getDriveMountPath(Long userId, Long assetId, String sessionId) { + return this.buildDriveMountPath(DEFAULT_S, userId, DEFAULT_N, DEFAULT_S); + } + }, + + /** + * 会话维度 + */ + SESSION("SE") { + @Override + public String getDriveMountPath(Long userId, Long assetId, String sessionId) { + return this.buildDriveMountPath(Dates.current(Dates.YMD2), userId, assetId, sessionId); + } + }, + + /** + * 资产维度 + */ + ASSET("A") { + @Override + public String getDriveMountPath(Long userId, Long assetId, String sessionId) { + return this.buildDriveMountPath(DEFAULT_S, userId, assetId, DEFAULT_S); + } + }, + + /** + * 天维度 + */ + DAY("D") { + @Override + public String getDriveMountPath(Long userId, Long assetId, String sessionId) { + return this.buildDriveMountPath(Dates.current(Dates.YMD2), userId, DEFAULT_N, DEFAULT_S); + } + }, + + /** + * 天维度 + 资产维度 + */ + DAY_ASSET("DA") { + @Override + public String getDriveMountPath(Long userId, Long assetId, String sessionId) { + return this.buildDriveMountPath(Dates.current(Dates.YMD2), userId, DEFAULT_N, DEFAULT_S); + } + }, + + /** + * 月维度 + */ + MONTH("M") { + @Override + public String getDriveMountPath(Long userId, Long assetId, String sessionId) { + String date = Dates.stream() + .setDay(1) + .format(Dates.YMD2); + return this.buildDriveMountPath(date, userId, DEFAULT_N, DEFAULT_S); + } + }, + + /** + * 月维度 + 资产维度 + */ + MONTH_ASSET("MA") { + @Override + public String getDriveMountPath(Long userId, Long assetId, String sessionId) { + String date = Dates.stream() + .setDay(1) + .format(Dates.YMD2); + return this.buildDriveMountPath(date, userId, assetId, DEFAULT_S); + } + }, + + ; + + private static final Long DEFAULT_N = 0L; + + private static final String DEFAULT_S = "0"; + + private final String prefix; + + /** + * 获取驱动挂载路径 + * + * @param userId userId + * @param assetId assetId + * @param sessionId sessionId + * @return path + */ + public abstract String getDriveMountPath(Long userId, Long assetId, String sessionId); + + /** + * 构建驱动挂载路径 + * + * @param time time + * @param userId userId + * @param assetId assetId + * @param sessionId sessionId + * @return path + */ + protected String buildDriveMountPath(String time, Long userId, Long assetId, String sessionId) { + return prefix + "_" + + time + "_" + + userId + "_" + + assetId + "_" + + sessionId; + } + + public static DriveMountModeEnum of(String mode) { + if (mode == null) { + return ASSET; + } + for (DriveMountModeEnum value : values()) { + if (value.name().equalsIgnoreCase(mode)) { + return value; + } + } + return ASSET; + } + +} diff --git a/orion-visor-modules/orion-visor-module-terminal/orion-visor-module-terminal-service/src/main/java/org/dromara/visor/module/terminal/handler/guacd/constant/GuacdConst.java b/orion-visor-modules/orion-visor-module-terminal/orion-visor-module-terminal-service/src/main/java/org/dromara/visor/module/terminal/handler/guacd/constant/GuacdConst.java index 16c927ec..c0edfb9e 100644 --- a/orion-visor-modules/orion-visor-module-terminal/orion-visor-module-terminal-service/src/main/java/org/dromara/visor/module/terminal/handler/guacd/constant/GuacdConst.java +++ b/orion-visor-modules/orion-visor-module-terminal/orion-visor-module-terminal-service/src/main/java/org/dromara/visor/module/terminal/handler/guacd/constant/GuacdConst.java @@ -306,10 +306,15 @@ public interface GuacdConst { String DISABLE_OFFSCREEN_CACHING = "disable-offscreen-caching"; /** - * 禁用字形缓存 boolean 默认禁用 + * 禁用字形缓存 boolean */ String DISABLE_GLYPH_CACHING = "disable-glyph-caching"; + /** + * 禁用图形加速 boolean + */ + String DISABLE_GFX = "disable-gfx"; + /** * 远程应用名称 */ diff --git a/orion-visor-modules/orion-visor-module-terminal/orion-visor-module-terminal-service/src/main/java/org/dromara/visor/module/terminal/handler/terminal/enums/InputProtocolEnum.java b/orion-visor-modules/orion-visor-module-terminal/orion-visor-module-terminal-service/src/main/java/org/dromara/visor/module/terminal/handler/terminal/enums/InputProtocolEnum.java index 4a192be7..158b24bc 100644 --- a/orion-visor-modules/orion-visor-module-terminal/orion-visor-module-terminal-service/src/main/java/org/dromara/visor/module/terminal/handler/terminal/enums/InputProtocolEnum.java +++ b/orion-visor-modules/orion-visor-module-terminal/orion-visor-module-terminal-service/src/main/java/org/dromara/visor/module/terminal/handler/terminal/enums/InputProtocolEnum.java @@ -43,7 +43,7 @@ import javax.annotation.PostConstruct; public enum InputProtocolEnum { /** - * 连接终端 + * 请求连接 */ CONNECT("co", TerminalConnectHandler.class, @@ -74,7 +74,17 @@ public enum InputProtocolEnum { new String[]{"type", "width", "height"}, TerminalResizeRequest.class), - // ----------------------- SSH ---------------------- + // ----------------------- guacd ---------------------- + + /** + * guacd 指令 + */ + GUACD_INSTRUCTION("gi", + GuacdInstructionHandler.class, + new String[]{"type", "instruction"}, + GuacdInstructionRequest.class), + + // ----------------------- ssh ---------------------- /** * SSH 输入 @@ -84,7 +94,7 @@ public enum InputProtocolEnum { new String[]{"type", "command"}, SshInputRequest.class), - // ----------------------- SFTP ---------------------- + // ----------------------- sftp ---------------------- /** * SFTP 文件列表 @@ -182,15 +192,15 @@ public enum InputProtocolEnum { new String[]{"type", "path"}, SftpBaseRequest.class), - // ----------------------- guacd ---------------------- + // ----------------------- rdp ---------------------- /** - * guacd 指令 + * RDP 文件系统事件 */ - GUACD_INSTRUCTION("gi", - GuacdInstructionHandler.class, - new String[]{"type", "instruction"}, - GuacdInstructionRequest.class), + RDP_FILE_SYSTEM_EVENT("fse", + RdpFileSystemEventHandler.class, + new String[]{"type", "event"}, + RdpFileSystemEventRequest.class), ; diff --git a/orion-visor-modules/orion-visor-module-terminal/orion-visor-module-terminal-service/src/main/java/org/dromara/visor/module/terminal/handler/terminal/enums/OutputProtocolEnum.java b/orion-visor-modules/orion-visor-module-terminal/orion-visor-module-terminal-service/src/main/java/org/dromara/visor/module/terminal/handler/terminal/enums/OutputProtocolEnum.java index 99abd79b..1b469ad0 100644 --- a/orion-visor-modules/orion-visor-module-terminal/orion-visor-module-terminal-service/src/main/java/org/dromara/visor/module/terminal/handler/terminal/enums/OutputProtocolEnum.java +++ b/orion-visor-modules/orion-visor-module-terminal/orion-visor-module-terminal-service/src/main/java/org/dromara/visor/module/terminal/handler/terminal/enums/OutputProtocolEnum.java @@ -69,6 +69,13 @@ public enum OutputProtocolEnum { */ RESIZE("rs", "${type}|${width}|${height}"), + // ----------------------- guacd ---------------------- + + /** + * guacd 指令 + */ + GUACD_INSTRUCTION("gi", "${type}|${instruction}"), + // ----------------------- ssh ---------------------- /** @@ -143,13 +150,6 @@ public enum OutputProtocolEnum { */ SFTP_SET_CONTENT("sc", "${type}|${result}|${msg}|${token}"), - // ----------------------- guacd ---------------------- - - /** - * guacd 指令 - */ - GUACD_INSTRUCTION("gi", "${type}|${instruction}"), - ; private final String type; diff --git a/orion-visor-modules/orion-visor-module-terminal/orion-visor-module-terminal-service/src/main/java/org/dromara/visor/module/terminal/handler/terminal/handler/RdpFileSystemEventHandler.java b/orion-visor-modules/orion-visor-module-terminal/orion-visor-module-terminal-service/src/main/java/org/dromara/visor/module/terminal/handler/terminal/handler/RdpFileSystemEventHandler.java new file mode 100644 index 00000000..34f0b485 --- /dev/null +++ b/orion-visor-modules/orion-visor-module-terminal/orion-visor-module-terminal-service/src/main/java/org/dromara/visor/module/terminal/handler/terminal/handler/RdpFileSystemEventHandler.java @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2023 - present Dromara, All rights reserved. + * + * https://visor.dromara.org + * https://visor.dromara.org.cn + * https://visor.orionsec.cn + * + * Members: + * Jiahang Li - ljh1553488six@139.com - author + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dromara.visor.module.terminal.handler.terminal.handler; + +import cn.orionsec.kit.lang.utils.collect.Maps; +import com.alibaba.fastjson.JSON; +import lombok.extern.slf4j.Slf4j; +import org.dromara.visor.framework.biz.operator.log.core.utils.OperatorLogs; +import org.dromara.visor.module.terminal.define.operator.TerminalOperatorType; +import org.dromara.visor.module.terminal.handler.terminal.model.TerminalChannelProps; +import org.dromara.visor.module.terminal.handler.terminal.model.request.RdpFileSystemEventRequest; +import org.dromara.visor.module.terminal.handler.terminal.model.transport.RdpFileSystemEvent; +import org.dromara.visor.module.terminal.handler.terminal.sender.IGuacdTerminalSender; +import org.springframework.stereotype.Component; + +import java.util.Map; + +/** + * rdp 文件系统事件 处理器 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2024/2/19 11:13 + */ +@Slf4j +@Component +public class RdpFileSystemEventHandler extends AbstractTerminalHandler { + + @Override + public void handle(TerminalChannelProps props, IGuacdTerminalSender sender, RdpFileSystemEventRequest payload) { + long startTime = System.currentTimeMillis(); + String sessionId = props.getId(); + // 获取会话 + RdpFileSystemEvent fsEvent = JSON.parseObject(payload.getEvent(), RdpFileSystemEvent.class); + String event = fsEvent.getEvent(); + String path = fsEvent.getPath(); + log.info("RdpFileSystemEventHandler-handle start sessionId: {}, event: {}, path: {}", sessionId, event, path); + String operatorType; + if (TerminalOperatorType.RDP_UPLOAD.equals(event)) { + // 上传文件 + operatorType = TerminalOperatorType.RDP_UPLOAD; + } else if (TerminalOperatorType.RDP_DOWNLOAD.equals(event)) { + // 下载文件 + operatorType = TerminalOperatorType.RDP_DOWNLOAD; + } else { + return; + } + // 保存操作日志 + Map extra = Maps.newMap(); + extra.put(OperatorLogs.PATH, path); + this.saveOperatorLog(props, extra, operatorType, startTime, null); + } + +} diff --git a/orion-visor-modules/orion-visor-module-terminal/orion-visor-module-terminal-service/src/main/java/org/dromara/visor/module/terminal/handler/terminal/handler/SftpDownloadFlatDirectoryHandler.java b/orion-visor-modules/orion-visor-module-terminal/orion-visor-module-terminal-service/src/main/java/org/dromara/visor/module/terminal/handler/terminal/handler/SftpDownloadFlatDirectoryHandler.java index eaa03ac8..6dc9b143 100644 --- a/orion-visor-modules/orion-visor-module-terminal/orion-visor-module-terminal-service/src/main/java/org/dromara/visor/module/terminal/handler/terminal/handler/SftpDownloadFlatDirectoryHandler.java +++ b/orion-visor-modules/orion-visor-module-terminal/orion-visor-module-terminal-service/src/main/java/org/dromara/visor/module/terminal/handler/terminal/handler/SftpDownloadFlatDirectoryHandler.java @@ -29,6 +29,7 @@ import org.dromara.visor.module.terminal.handler.terminal.model.request.SftpDown import org.dromara.visor.module.terminal.handler.terminal.model.response.SftpFileVO; import org.dromara.visor.module.terminal.handler.terminal.sender.ISftpTerminalSender; import org.dromara.visor.module.terminal.handler.terminal.session.ISftpSession; +import org.dromara.visor.module.terminal.utils.SftpFileUtils; import org.springframework.stereotype.Component; import java.util.Arrays; @@ -50,7 +51,7 @@ public class SftpDownloadFlatDirectoryHandler extends AbstractTerminalHandler files = Lists.empty(); diff --git a/orion-visor-modules/orion-visor-module-terminal/orion-visor-module-terminal-service/src/main/java/org/dromara/visor/module/terminal/handler/terminal/handler/SftpRemoveHandler.java b/orion-visor-modules/orion-visor-module-terminal/orion-visor-module-terminal-service/src/main/java/org/dromara/visor/module/terminal/handler/terminal/handler/SftpRemoveHandler.java index 81693ce7..df595b10 100644 --- a/orion-visor-modules/orion-visor-module-terminal/orion-visor-module-terminal-service/src/main/java/org/dromara/visor/module/terminal/handler/terminal/handler/SftpRemoveHandler.java +++ b/orion-visor-modules/orion-visor-module-terminal/orion-visor-module-terminal-service/src/main/java/org/dromara/visor/module/terminal/handler/terminal/handler/SftpRemoveHandler.java @@ -24,12 +24,14 @@ package org.dromara.visor.module.terminal.handler.terminal.handler; import cn.orionsec.kit.lang.utils.collect.Maps; import lombok.extern.slf4j.Slf4j; +import org.dromara.visor.common.constant.Const; import org.dromara.visor.framework.biz.operator.log.core.utils.OperatorLogs; import org.dromara.visor.module.terminal.define.operator.TerminalOperatorType; import org.dromara.visor.module.terminal.handler.terminal.model.TerminalChannelProps; import org.dromara.visor.module.terminal.handler.terminal.model.request.SftpBaseRequest; import org.dromara.visor.module.terminal.handler.terminal.sender.ISftpTerminalSender; import org.dromara.visor.module.terminal.handler.terminal.session.ISftpSession; +import org.dromara.visor.module.terminal.utils.SftpFileUtils; import org.springframework.stereotype.Component; import java.util.Arrays; @@ -49,11 +51,11 @@ public class SftpRemoveHandler extends AbstractTerminalHandler extra = Maps.newMap(); extra.put(OperatorLogs.PATH, path); + extra.put(OperatorLogs.COUNT, paths.length); this.saveOperatorLog(props, extra, TerminalOperatorType.SFTP_REMOVE, startTime, ex); diff --git a/orion-visor-modules/orion-visor-module-terminal/orion-visor-module-terminal-service/src/main/java/org/dromara/visor/module/terminal/handler/terminal/handler/TerminalConnectHandler.java b/orion-visor-modules/orion-visor-module-terminal/orion-visor-module-terminal-service/src/main/java/org/dromara/visor/module/terminal/handler/terminal/handler/TerminalConnectHandler.java index d2aab8b7..14555223 100644 --- a/orion-visor-modules/orion-visor-module-terminal/orion-visor-module-terminal-service/src/main/java/org/dromara/visor/module/terminal/handler/terminal/handler/TerminalConnectHandler.java +++ b/orion-visor-modules/orion-visor-module-terminal/orion-visor-module-terminal-service/src/main/java/org/dromara/visor/module/terminal/handler/terminal/handler/TerminalConnectHandler.java @@ -129,6 +129,7 @@ public class TerminalConnectHandler extends AbstractTerminalHandler i tunnel.setParameter(GuacdConst.ENABLE_FULL_WINDOW_DRAG, extra.getEnableFullWindowDrag()); tunnel.setParameter(GuacdConst.ENABLE_DESKTOP_COMPOSITION, extra.getEnableDesktopComposition()); tunnel.setParameter(GuacdConst.ENABLE_MENU_ANIMATIONS, extra.getEnableMenuAnimations()); + tunnel.setParameter(GuacdConst.DISABLE_BITMAP_CACHING, extra.getDisableBitmapCaching()); tunnel.setParameter(GuacdConst.DISABLE_OFFSCREEN_CACHING, extra.getDisableOffscreenCaching()); tunnel.setParameter(GuacdConst.DISABLE_GLYPH_CACHING, extra.getDisableGlyphCaching()); - tunnel.setParameter(GuacdConst.DISABLE_BITMAP_CACHING, extra.getDisableBitmapCaching()); + tunnel.setParameter(GuacdConst.DISABLE_GFX, extra.getDisableGfx()); // 音频 tunnel.setAudioMimeTypes(GuacdConst.AUDIO_MIMETYPES); tunnel.setParameter(GuacdConst.ENABLE_AUDIO_INPUT, extra.getEnableAudioInput()); @@ -113,8 +114,15 @@ public class RdpSession extends AbstractGuacdSession i tunnel.setParameter(GuacdConst.ENABLE_DRIVE, true); tunnel.setParameter(GuacdConst.CREATE_DRIVE_PATH, true); tunnel.setParameter(GuacdConst.DRIVE_NAME, GuacdConst.DRIVE_DRIVE_NAME); - // 父文件夹必须存在 所以只能用 _ 分 - tunnel.setParameter(GuacdConst.DRIVE_PATH, Files1.getPath(guacdConfig.getDrivePath() + "/" + Dates.current(Dates.YMD2) + "_" + props.getUserId() + "_" + props.getHostId())); + // 父文件夹必须存在 否则会报错 所以不能分层 + String driveMountPath = DriveMountModeEnum.of(extra.getDriveMountMode()) + .getDriveMountPath(props.getUserId(), props.getHostId(), props.getId()); + tunnel.setParameter(GuacdConst.DRIVE_PATH, Files1.getPath(guacdConfig.getDrivePath() + "/" + driveMountPath)); + // 初始化程序 + String initialProgram = config.getInitialProgram(); + if (!Strings.isBlank(initialProgram)) { + tunnel.setParameter(GuacdConst.INITIAL_PROGRAM, initialProgram); + } // 预连接 String preConnectionId = config.getPreConnectionId(); if (!Strings.isBlank(preConnectionId)) { @@ -153,8 +161,9 @@ public class RdpSession extends AbstractGuacdSession i extra.setEnableDesktopComposition(false); extra.setEnableMenuAnimations(false); extra.setDisableBitmapCaching(false); + extra.setDisableOffscreenCaching(false); extra.setDisableGlyphCaching(false); - extra.setDisableBitmapCaching(false); + extra.setDisableGfx(false); extra.setEnableAudioInput(false); extra.setEnableAudioOutput(false); } diff --git a/orion-visor-modules/orion-visor-module-terminal/orion-visor-module-terminal-service/src/main/java/org/dromara/visor/module/terminal/handler/terminal/session/SftpSession.java b/orion-visor-modules/orion-visor-module-terminal/orion-visor-module-terminal-service/src/main/java/org/dromara/visor/module/terminal/handler/terminal/session/SftpSession.java index 0fd5e7b2..75367f9b 100644 --- a/orion-visor-modules/orion-visor-module-terminal/orion-visor-module-terminal-service/src/main/java/org/dromara/visor/module/terminal/handler/terminal/session/SftpSession.java +++ b/orion-visor-modules/orion-visor-module-terminal/orion-visor-module-terminal-service/src/main/java/org/dromara/visor/module/terminal/handler/terminal/session/SftpSession.java @@ -34,7 +34,6 @@ import lombok.extern.slf4j.Slf4j; import org.dromara.visor.common.constant.Const; import org.dromara.visor.common.constant.ErrorMessage; import org.dromara.visor.common.utils.Valid; -import org.dromara.visor.module.common.utils.SftpUtils; import org.dromara.visor.module.terminal.handler.terminal.model.TerminalChannelProps; import org.dromara.visor.module.terminal.handler.terminal.model.config.TerminalSessionSftpConfig; import org.dromara.visor.module.terminal.handler.terminal.model.response.SftpFileVO; @@ -117,7 +116,7 @@ public class SftpSession extends AbstractTerminalSession paths; - /** * 错误信息 后端赋值 */ diff --git a/orion-visor-modules/orion-visor-module-terminal/orion-visor-module-terminal-service/src/main/java/org/dromara/visor/module/terminal/handler/transfer/session/DownloadSession.java b/orion-visor-modules/orion-visor-module-terminal/orion-visor-module-terminal-service/src/main/java/org/dromara/visor/module/terminal/handler/transfer/session/DownloadSession.java index 6e57bc80..96795232 100644 --- a/orion-visor-modules/orion-visor-module-terminal/orion-visor-module-terminal-service/src/main/java/org/dromara/visor/module/terminal/handler/transfer/session/DownloadSession.java +++ b/orion-visor-modules/orion-visor-module-terminal/orion-visor-module-terminal-service/src/main/java/org/dromara/visor/module/terminal/handler/transfer/session/DownloadSession.java @@ -25,6 +25,7 @@ package org.dromara.visor.module.terminal.handler.transfer.session; 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.collect.Lists; import cn.orionsec.kit.lang.utils.io.Streams; import cn.orionsec.kit.net.host.SessionStore; import cn.orionsec.kit.net.host.sftp.SftpFile; @@ -72,7 +73,7 @@ public class DownloadSession extends TransferSession implements StreamingRespons super.onStart(request); log.info("DownloadSession.startDownload open start channelId: {}, path: {}", channelId, path); // 保存操作日志 - this.saveOperatorLog(TerminalOperatorType.SFTP_DOWNLOAD, path); + this.saveOperatorLog(request.getLogId(), TerminalOperatorType.SFTP_DOWNLOAD, Lists.singleton(path)); // 检查连接 this.init(); // 检查文件是否存在 diff --git a/orion-visor-modules/orion-visor-module-terminal/orion-visor-module-terminal-service/src/main/java/org/dromara/visor/module/terminal/handler/transfer/session/TransferSession.java b/orion-visor-modules/orion-visor-module-terminal/orion-visor-module-terminal-service/src/main/java/org/dromara/visor/module/terminal/handler/transfer/session/TransferSession.java index a65240fb..7f3e942b 100644 --- a/orion-visor-modules/orion-visor-module-terminal/orion-visor-module-terminal-service/src/main/java/org/dromara/visor/module/terminal/handler/transfer/session/TransferSession.java +++ b/orion-visor-modules/orion-visor-module-terminal/orion-visor-module-terminal-service/src/main/java/org/dromara/visor/module/terminal/handler/transfer/session/TransferSession.java @@ -30,6 +30,7 @@ import cn.orionsec.kit.net.host.sftp.SftpExecutor; import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; +import org.dromara.visor.common.constant.Const; import org.dromara.visor.common.constant.FieldConst; import org.dromara.visor.common.session.config.SshConnectConfig; import org.dromara.visor.framework.biz.operator.log.core.model.OperatorLogModel; @@ -43,6 +44,7 @@ import org.dromara.visor.module.terminal.handler.transfer.model.TransferOperator import org.dromara.visor.module.terminal.handler.transfer.utils.TransferUtils; import org.springframework.web.socket.WebSocketSession; +import java.util.List; import java.util.Map; /** @@ -147,20 +149,25 @@ public abstract class TransferSession implements ITransferSession { /** * 保存操作日志 * - * @param type type - * @param path path + * @param logId logId + * @param type type + * @param paths paths */ - protected void saveOperatorLog(String type, String path) { - // 设置参数 + protected void saveOperatorLog(Long logId, String type, List paths) { + TerminalChannelProps props = WebSockets.getAttr(channel, FieldConst.PROPS); + String path = String.join(Const.LF, paths); + int count = paths.size(); + // 获取操作日志 Map extra = Maps.newMap(); extra.put(OperatorLogs.PATH, path); + extra.put(OperatorLogs.COUNT, count); extra.put(OperatorLogs.HOST_ID, connectConfig.getHostId()); extra.put(OperatorLogs.HOST_NAME, connectConfig.getHostName()); extra.put(OperatorLogs.ADDRESS, connectConfig.getHostAddress()); - // 获取日志 - TerminalChannelProps props = WebSockets.getAttr(channel, FieldConst.PROPS); OperatorLogModel model = TerminalUtils.getOperatorLogModel(props, extra, type, System.currentTimeMillis(), null); - // 保存 + // 保存操作日志 + TerminalAsyncSaver.saveOperatorLog(model); + // 保存操作日志 TerminalAsyncSaver.saveOperatorLog(model); } diff --git a/orion-visor-modules/orion-visor-module-terminal/orion-visor-module-terminal-service/src/main/java/org/dromara/visor/module/terminal/handler/transfer/session/UploadSession.java b/orion-visor-modules/orion-visor-module-terminal/orion-visor-module-terminal-service/src/main/java/org/dromara/visor/module/terminal/handler/transfer/session/UploadSession.java index 30955a2b..447aa69b 100644 --- a/orion-visor-modules/orion-visor-module-terminal/orion-visor-module-terminal-service/src/main/java/org/dromara/visor/module/terminal/handler/transfer/session/UploadSession.java +++ b/orion-visor-modules/orion-visor-module-terminal/orion-visor-module-terminal-service/src/main/java/org/dromara/visor/module/terminal/handler/transfer/session/UploadSession.java @@ -22,6 +22,7 @@ */ package org.dromara.visor.module.terminal.handler.transfer.session; +import cn.orionsec.kit.lang.utils.collect.Lists; import cn.orionsec.kit.lang.utils.io.Streams; import cn.orionsec.kit.net.host.SessionStore; import lombok.extern.slf4j.Slf4j; @@ -58,7 +59,7 @@ public class UploadSession extends TransferSession { try { log.info("UploadSession.startUpload start channelId: {}, path: {}", channelId, path); // 保存操作日志 - this.saveOperatorLog(TerminalOperatorType.SFTP_UPLOAD, path); + this.saveOperatorLog(request.getLogId(), TerminalOperatorType.SFTP_UPLOAD, Lists.singleton(path)); // 检查连接 this.init(); // 检查文件是否存在 diff --git a/orion-visor-modules/orion-visor-module-terminal/orion-visor-module-terminal-service/src/main/java/org/dromara/visor/module/terminal/service/TerminalFileLogService.java b/orion-visor-modules/orion-visor-module-terminal/orion-visor-module-terminal-service/src/main/java/org/dromara/visor/module/terminal/service/TerminalFileLogService.java new file mode 100644 index 00000000..78aa9cac --- /dev/null +++ b/orion-visor-modules/orion-visor-module-terminal/orion-visor-module-terminal-service/src/main/java/org/dromara/visor/module/terminal/service/TerminalFileLogService.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2023 - present Dromara, All rights reserved. + * + * https://visor.dromara.org + * https://visor.dromara.org.cn + * https://visor.orionsec.cn + * + * Members: + * Jiahang Li - ljh1553488six@139.com - author + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dromara.visor.module.terminal.service; + +import cn.orionsec.kit.lang.define.wrapper.DataGrid; +import org.dromara.visor.module.terminal.entity.request.terminal.TerminalFileLogQueryRequest; +import org.dromara.visor.module.terminal.entity.vo.TerminalFileLogVO; + +import java.util.List; + +/** + * 终端文件日志服务类 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2023-12-26 22:09 + */ +public interface TerminalFileLogService { + + /** + * 分页查询终端文件操作日志 + * + * @param request request + * @return rows + */ + DataGrid getTerminalFileLogPage(TerminalFileLogQueryRequest request); + + /** + * 获取终端文件操作日志数量 + * + * @param request request + * @return count + */ + Long getTerminalFileLogCount(TerminalFileLogQueryRequest request); + + /** + * 删除终端文件操作日志 + * + * @param idList idList + * @return effect + */ + Integer deleteTerminalFileLog(List idList); + +} diff --git a/orion-visor-modules/orion-visor-module-terminal/orion-visor-module-terminal-service/src/main/java/org/dromara/visor/module/terminal/service/TerminalSftpService.java b/orion-visor-modules/orion-visor-module-terminal/orion-visor-module-terminal-service/src/main/java/org/dromara/visor/module/terminal/service/TerminalSftpService.java index 7d1fe1fb..14f3fabc 100644 --- a/orion-visor-modules/orion-visor-module-terminal/orion-visor-module-terminal-service/src/main/java/org/dromara/visor/module/terminal/service/TerminalSftpService.java +++ b/orion-visor-modules/orion-visor-module-terminal/orion-visor-module-terminal-service/src/main/java/org/dromara/visor/module/terminal/service/TerminalSftpService.java @@ -22,15 +22,11 @@ */ package org.dromara.visor.module.terminal.service; -import cn.orionsec.kit.lang.define.wrapper.DataGrid; -import org.dromara.visor.module.terminal.entity.request.terminal.TerminalSftpLogQueryRequest; -import org.dromara.visor.module.terminal.entity.vo.TerminalSftpLogVO; import org.springframework.web.multipart.MultipartFile; import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import javax.servlet.http.HttpServletResponse; import java.io.IOException; -import java.util.List; /** * SFTP 操作 服务类 @@ -41,30 +37,6 @@ import java.util.List; */ public interface TerminalSftpService { - /** - * 分页查询 SFTP 操作日志 - * - * @param request request - * @return rows - */ - DataGrid getTerminalSftpLogPage(TerminalSftpLogQueryRequest request); - - /** - * 获取 SFTP 操作日志数量 - * - * @param request request - * @return count - */ - Long getTerminalSftpLogCount(TerminalSftpLogQueryRequest request); - - /** - * 删除 SFTP 操作日志 - * - * @param idList idList - * @return effect - */ - Integer deleteTerminalSftpLog(List idList); - /** * 设置文件内容 * diff --git a/orion-visor-modules/orion-visor-module-terminal/orion-visor-module-terminal-service/src/main/java/org/dromara/visor/module/terminal/service/impl/TerminalFileLogServiceImpl.java b/orion-visor-modules/orion-visor-module-terminal/orion-visor-module-terminal-service/src/main/java/org/dromara/visor/module/terminal/service/impl/TerminalFileLogServiceImpl.java new file mode 100644 index 00000000..d8516115 --- /dev/null +++ b/orion-visor-modules/orion-visor-module-terminal/orion-visor-module-terminal-service/src/main/java/org/dromara/visor/module/terminal/service/impl/TerminalFileLogServiceImpl.java @@ -0,0 +1,131 @@ +/* + * Copyright (c) 2023 - present Dromara, All rights reserved. + * + * https://visor.dromara.org + * https://visor.dromara.org.cn + * https://visor.orionsec.cn + * + * Members: + * Jiahang Li - ljh1553488six@139.com - author + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dromara.visor.module.terminal.service.impl; + +import cn.orionsec.kit.lang.define.wrapper.DataGrid; +import cn.orionsec.kit.lang.utils.Arrays1; +import cn.orionsec.kit.lang.utils.Strings; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import lombok.extern.slf4j.Slf4j; +import org.dromara.visor.common.constant.ExtraFieldConst; +import org.dromara.visor.framework.biz.operator.log.core.utils.OperatorLogs; +import org.dromara.visor.module.infra.api.OperatorLogApi; +import org.dromara.visor.module.infra.entity.dto.operator.OperatorLogQueryDTO; +import org.dromara.visor.module.terminal.convert.TerminalFileLogConvert; +import org.dromara.visor.module.terminal.define.operator.TerminalFileLogOperatorType; +import org.dromara.visor.module.terminal.entity.request.terminal.TerminalFileLogQueryRequest; +import org.dromara.visor.module.terminal.entity.vo.TerminalFileLogVO; +import org.dromara.visor.module.terminal.service.TerminalFileLogService; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.List; +import java.util.Optional; + +/** + * 终端文件日志服务实现类 + * + * @author Jiahang Li + * @version 1.0.0 + * @since 2024/3/4 23:35 + */ +@Slf4j +@Service +public class TerminalFileLogServiceImpl implements TerminalFileLogService { + + @Resource + private OperatorLogApi operatorLogApi; + + @Override + public DataGrid getTerminalFileLogPage(TerminalFileLogQueryRequest request) { + // 查询 + OperatorLogQueryDTO query = this.buildQueryInfo(request); + // 转换 + return operatorLogApi.getOperatorLogPage(query) + .map(s -> { + JSONObject extra = JSON.parseObject(s.getExtra()); + TerminalFileLogVO vo = TerminalFileLogConvert.MAPPER.to(s); + vo.setHostId(extra.getLong(ExtraFieldConst.HOST_ID)); + vo.setHostName(extra.getString(ExtraFieldConst.HOST_NAME)); + vo.setHostAddress(extra.getString(ExtraFieldConst.ADDRESS)); + String[] paths = Optional.ofNullable(extra.getString(ExtraFieldConst.PATH)) + .map(p -> p.split("\\|")) + .orElse(new String[0]); + vo.setPaths(paths); + vo.setExtra(extra); + return vo; + }); + } + + @Override + public Long getTerminalFileLogCount(TerminalFileLogQueryRequest request) { + // 查询 + OperatorLogQueryDTO query = this.buildQueryInfo(request); + // 转换 + return operatorLogApi.getOperatorLogCount(query); + } + + @Override + public Integer deleteTerminalFileLog(List idList) { + log.info("TerminalSftpService.deleteTerminalFileLog start {}", JSON.toJSONString(idList)); + Integer effect = operatorLogApi.deleteOperatorLog(idList); + log.info("TerminalSftpService.deleteTerminalFileLog finish {}", effect); + // 设置日志参数 + OperatorLogs.add(OperatorLogs.COUNT, effect); + return effect; + } + + /** + * 构建查询对象 + * + * @param request request + * @return query + */ + private OperatorLogQueryDTO buildQueryInfo(TerminalFileLogQueryRequest request) { + Long hostId = request.getHostId(); + String type = request.getType(); + // 构建参数 + OperatorLogQueryDTO query = OperatorLogQueryDTO.builder() + .userId(request.getUserId()) + .result(request.getResult()) + .startTimeStart(Arrays1.getIfPresent(request.getStartTimeRange(), 0)) + .startTimeEnd(Arrays1.getIfPresent(request.getStartTimeRange(), 1)) + .build(); + query.setPage(request.getPage()); + query.setLimit(request.getLimit()); + query.setOrder(request.getOrder()); + if (Strings.isBlank(type)) { + // 查询全部文件操作类型 + query.setTypeList(TerminalFileLogOperatorType.TYPES); + } else { + query.setType(type); + } + // 模糊查询 + if (hostId != null) { + query.setExtra("\"hostId\": " + hostId + ","); + } + return query; + } + +} diff --git a/orion-visor-modules/orion-visor-module-terminal/orion-visor-module-terminal-service/src/main/java/org/dromara/visor/module/terminal/service/impl/TerminalSftpServiceImpl.java b/orion-visor-modules/orion-visor-module-terminal/orion-visor-module-terminal-service/src/main/java/org/dromara/visor/module/terminal/service/impl/TerminalSftpServiceImpl.java index 566d32c3..ed427cde 100644 --- a/orion-visor-modules/orion-visor-module-terminal/orion-visor-module-terminal-service/src/main/java/org/dromara/visor/module/terminal/service/impl/TerminalSftpServiceImpl.java +++ b/orion-visor-modules/orion-visor-module-terminal/orion-visor-module-terminal-service/src/main/java/org/dromara/visor/module/terminal/service/impl/TerminalSftpServiceImpl.java @@ -23,9 +23,7 @@ package org.dromara.visor.module.terminal.service.impl; import cn.orionsec.kit.lang.constant.StandardContentType; -import cn.orionsec.kit.lang.define.wrapper.DataGrid; import cn.orionsec.kit.lang.define.wrapper.HttpWrapper; -import cn.orionsec.kit.lang.utils.Arrays1; import cn.orionsec.kit.lang.utils.Exceptions; import cn.orionsec.kit.lang.utils.Strings; import cn.orionsec.kit.lang.utils.Valid; @@ -34,27 +32,17 @@ 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.web.servlet.web.Servlets; -import com.alibaba.fastjson.JSON; -import com.alibaba.fastjson.JSONObject; import lombok.extern.slf4j.Slf4j; import org.dromara.visor.common.constant.Const; import org.dromara.visor.common.constant.ErrorMessage; -import org.dromara.visor.common.constant.ExtraFieldConst; import org.dromara.visor.common.session.config.SshConnectConfig; import org.dromara.visor.common.session.ssh.SessionStores; -import org.dromara.visor.framework.biz.operator.log.core.utils.OperatorLogs; import org.dromara.visor.framework.redis.core.utils.RedisStrings; import org.dromara.visor.framework.security.core.utils.SecurityUtils; import org.dromara.visor.module.asset.api.HostConnectApi; -import org.dromara.visor.module.infra.api.OperatorLogApi; -import org.dromara.visor.module.infra.entity.dto.operator.OperatorLogQueryDTO; -import org.dromara.visor.module.terminal.convert.TerminalSftpLogConvert; import org.dromara.visor.module.terminal.define.cache.TerminalCacheKeyDefine; -import org.dromara.visor.module.terminal.define.operator.TerminalOperatorType; import org.dromara.visor.module.terminal.entity.dto.SftpGetContentCacheDTO; import org.dromara.visor.module.terminal.entity.dto.SftpSetContentCacheDTO; -import org.dromara.visor.module.terminal.entity.request.terminal.TerminalSftpLogQueryRequest; -import org.dromara.visor.module.terminal.entity.vo.TerminalSftpLogVO; import org.dromara.visor.module.terminal.handler.transfer.manager.TerminalTransferManager; import org.dromara.visor.module.terminal.handler.transfer.session.DownloadSession; import org.dromara.visor.module.terminal.service.TerminalSftpService; @@ -67,7 +55,6 @@ import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.util.List; import java.util.Optional; /** @@ -81,54 +68,12 @@ import java.util.Optional; @Service public class TerminalSftpServiceImpl implements TerminalSftpService { - @Resource - private OperatorLogApi operatorLogApi; - @Resource private HostConnectApi hostConnectApi; @Resource private TerminalTransferManager terminalTransferManager; - @Override - public DataGrid getTerminalSftpLogPage(TerminalSftpLogQueryRequest request) { - // 查询 - OperatorLogQueryDTO query = this.buildQueryInfo(request); - // 转换 - return operatorLogApi.getOperatorLogPage(query) - .map(s -> { - JSONObject extra = JSON.parseObject(s.getExtra()); - TerminalSftpLogVO vo = TerminalSftpLogConvert.MAPPER.to(s); - vo.setHostId(extra.getLong(ExtraFieldConst.HOST_ID)); - vo.setHostName(extra.getString(ExtraFieldConst.HOST_NAME)); - vo.setHostAddress(extra.getString(ExtraFieldConst.ADDRESS)); - String[] paths = Optional.ofNullable(extra.getString(ExtraFieldConst.PATH)) - .map(p -> p.split("\\|")) - .orElse(new String[0]); - vo.setPaths(paths); - vo.setExtra(extra); - return vo; - }); - } - - @Override - public Long getTerminalSftpLogCount(TerminalSftpLogQueryRequest request) { - // 查询 - OperatorLogQueryDTO query = this.buildQueryInfo(request); - // 转换 - return operatorLogApi.getOperatorLogCount(query); - } - - @Override - public Integer deleteTerminalSftpLog(List idList) { - log.info("TerminalSftpService.deleteSftpLog start {}", JSON.toJSONString(idList)); - Integer effect = operatorLogApi.deleteOperatorLog(idList); - log.info("TerminalSftpService.deleteSftpLog finish {}", effect); - // 设置日志参数 - OperatorLogs.add(OperatorLogs.COUNT, effect); - return effect; - } - @Override public void getFileContentByToken(String token, HttpServletResponse response) throws IOException { // 解析 token @@ -215,36 +160,4 @@ public class TerminalSftpServiceImpl implements TerminalSftpService { return session; } - /** - * 构建查询对象 - * - * @param request request - * @return query - */ - private OperatorLogQueryDTO buildQueryInfo(TerminalSftpLogQueryRequest request) { - Long hostId = request.getHostId(); - String type = request.getType(); - // 构建参数 - OperatorLogQueryDTO query = OperatorLogQueryDTO.builder() - .userId(request.getUserId()) - .result(request.getResult()) - .startTimeStart(Arrays1.getIfPresent(request.getStartTimeRange(), 0)) - .startTimeEnd(Arrays1.getIfPresent(request.getStartTimeRange(), 1)) - .build(); - query.setPage(request.getPage()); - query.setLimit(request.getLimit()); - query.setOrder(request.getOrder()); - if (Strings.isBlank(type)) { - // 查询全部 SFTP 类型 - query.setTypeList(TerminalOperatorType.SFTP_TYPES); - } else { - query.setType(type); - } - // 模糊查询 - if (hostId != null) { - query.setExtra("\"hostId\": " + hostId + ","); - } - return query; - } - } diff --git a/orion-visor-modules/orion-visor-module-terminal/orion-visor-module-terminal-service/src/main/java/org/dromara/visor/module/terminal/utils/SftpFileUtils.java b/orion-visor-modules/orion-visor-module-terminal/orion-visor-module-terminal-service/src/main/java/org/dromara/visor/module/terminal/utils/SftpFileUtils.java index 27eb64e5..8c97c447 100644 --- a/orion-visor-modules/orion-visor-module-terminal/orion-visor-module-terminal-service/src/main/java/org/dromara/visor/module/terminal/utils/SftpFileUtils.java +++ b/orion-visor-modules/orion-visor-module-terminal/orion-visor-module-terminal-service/src/main/java/org/dromara/visor/module/terminal/utils/SftpFileUtils.java @@ -22,6 +22,7 @@ */ package org.dromara.visor.module.terminal.utils; +import cn.orionsec.kit.lang.constant.Letters; import cn.orionsec.kit.lang.utils.io.FileType; import cn.orionsec.kit.lang.utils.io.Files1; import cn.orionsec.kit.net.host.sftp.SftpFile; @@ -41,6 +42,33 @@ public class SftpFileUtils { private SftpFileUtils() { } + /** + * 获取移动目标路径 + * + * @param source source + * @param target target + * @return absolute target + */ + public static String getAbsoluteTargetPath(String source, String target) { + if (target.charAt(0) == Letters.SLASH) { + // 绝对路径 + return Files1.getPath(Files1.normalize(target)); + } else { + // 相对路径 + return Files1.getPath(Files1.normalize(Files1.getPath(source + "/../" + target))); + } + } + + /** + * 分割文件路径 + * + * @param path path + * @return paths + */ + public static String[] fromMultiPaths(String path) { + return path.split("\\|"); + } + /** * 转为文件 * diff --git a/orion-visor-ui/.env.development b/orion-visor-ui/.env.development index 1a56480a..18b59f51 100644 --- a/orion-visor-ui/.env.development +++ b/orion-visor-ui/.env.development @@ -3,4 +3,4 @@ VITE_API_BASE_URL=http://127.0.0.1:9200/orion-visor/api # websocket 路径 VITE_WS_BASE_URL=ws://127.0.0.1:9200/orion-visor/keep-alive # 版本号 -VITE_APP_VERSION=2.4.0 +VITE_APP_VERSION=2.4.1 diff --git a/orion-visor-ui/.env.production b/orion-visor-ui/.env.production index 75998f13..25daa624 100644 --- a/orion-visor-ui/.env.production +++ b/orion-visor-ui/.env.production @@ -3,4 +3,4 @@ VITE_API_BASE_URL=/orion-visor/api # websocket 路径 VITE_WS_BASE_URL=/orion-visor/keep-alive # 版本号 -VITE_APP_VERSION=2.4.0 +VITE_APP_VERSION=2.4.1 diff --git a/orion-visor-ui/libs/guacamole-common-js.js b/orion-visor-ui/libs/guacamole-common-js.js index 845dd632..09363803 100644 --- a/orion-visor-ui/libs/guacamole-common-js.js +++ b/orion-visor-ui/libs/guacamole-common-js.js @@ -58,13 +58,27 @@ Guacamole.ArrayBufferReader = function(stream) { // Receive blobs as array buffers stream.onblob = function(data) { - // Convert to ArrayBuffer - var binary = window.atob(data); - var arrayBuffer = new ArrayBuffer(binary.length); - var bufferView = new Uint8Array(arrayBuffer); + var arrayBuffer, bufferView; - for (var i = 0; i < binary.length; i++) - bufferView[i] = binary.charCodeAt(i); + // Use native methods for directly decoding base64 to an array buffer + // when possible + if(Uint8Array.fromBase64) { + bufferView = Uint8Array.fromBase64(data); + arrayBuffer = bufferView.buffer; + } + + // Rely on binary strings and manual conversions where native methods + // like fromBase64() are not available + else { + + var binary = window.atob(data); + arrayBuffer = new ArrayBuffer(binary.length); + bufferView = new Uint8Array(arrayBuffer); + + for (var i = 0; i < binary.length; i++) + bufferView[i] = binary.charCodeAt(i); + + } // Call handler, if present if(guac_reader.ondata) @@ -1833,14 +1847,7 @@ Guacamole.Client = function(tunnel) { var guac_client = this; - var STATE_IDLE = 0; - var STATE_CONNECTING = 1; - var STATE_WAITING = 2; - var STATE_CONNECTED = 3; - var STATE_DISCONNECTING = 4; - var STATE_DISCONNECTED = 5; - - var currentState = STATE_IDLE; + var currentState = Guacamole.Client.State.IDLE; var currentTimestamp = 0; @@ -1962,8 +1969,8 @@ Guacamole.Client = function(tunnel) { } function isConnected() { - return currentState == STATE_CONNECTED - || currentState == STATE_WAITING; + return currentState == Guacamole.Client.State.CONNECTED + || currentState == Guacamole.Client.State.WAITING; } /** @@ -2728,6 +2735,12 @@ Guacamole.Client = function(tunnel) { * @event * @param {!number} timestamp * The timestamp associated with the sync instruction. + * + * @param {!number} frames + * The number of frames that were considered or combined to produce the + * frame associated with this sync instruction, or zero if this value + * is not known or the remote desktop server provides no concept of + * frames. */ this.onsync = null; @@ -3456,6 +3469,7 @@ Guacamole.Client = function(tunnel) { 'sync': function(parameters) { var timestamp = parseInt(parameters[0]); + var frames = parameters[1] ? parseInt(parameters[1]) : 0; // Flush display, send sync when done display.flush(function displaySyncComplete() { @@ -3473,15 +3487,15 @@ Guacamole.Client = function(tunnel) { currentTimestamp = timestamp; } - }); + }, timestamp, frames); // If received first update, no longer waiting. - if(currentState === STATE_WAITING) - setState(STATE_CONNECTED); + if(currentState === Guacamole.Client.State.WAITING) + setState(Guacamole.Client.State.CONNECTED); // Call sync handler if defined if(guac_client.onsync) - guac_client.onsync(timestamp); + guac_client.onsync(timestamp, frames); }, @@ -3633,10 +3647,10 @@ Guacamole.Client = function(tunnel) { this.disconnect = function() { // Only attempt disconnection not disconnected. - if(currentState != STATE_DISCONNECTED - && currentState != STATE_DISCONNECTING) { + if(currentState != Guacamole.Client.State.DISCONNECTED + && currentState != Guacamole.Client.State.DISCONNECTING) { - setState(STATE_DISCONNECTING); + setState(Guacamole.Client.State.DISCONNECTING); // Stop sending keep-alive messages stopKeepAlive(); @@ -3644,7 +3658,7 @@ Guacamole.Client = function(tunnel) { // Send disconnect message and disconnect tunnel.sendMessage('disconnect'); tunnel.disconnect(); - setState(STATE_DISCONNECTED); + setState(Guacamole.Client.State.DISCONNECTED); } @@ -3663,12 +3677,12 @@ Guacamole.Client = function(tunnel) { */ this.connect = function(data) { - setState(STATE_CONNECTING); + setState(Guacamole.Client.State.CONNECTING); try { tunnel.connect(data); } catch (status) { - setState(STATE_IDLE); + setState(Guacamole.Client.State.IDLE); throw status; } @@ -3676,38 +3690,61 @@ Guacamole.Client = function(tunnel) { // still here, even if not active scheduleKeepAlive(); - setState(STATE_WAITING); + setState(Guacamole.Client.State.WAITING); }; }; -// SETTED +/** + * All possible Guacamole Client states. + * + * @type {!Object.} + */ Guacamole.Client.State = { + /** * The client is idle, with no active connection. + * + * @type number */ 'IDLE': 0, + /** * The client is in the process of establishing a connection. + * + * @type {!number} */ 'CONNECTING': 1, + /** * The client is waiting on further information or a remote server to * establish the connection. + * + * @type {!number} */ 'WAITING': 2, + /** * The client is actively connected to a remote server. + * + * @type {!number} */ 'CONNECTED': 3, + /** * The client is in the process of disconnecting from the remote server. + * + * @type {!number} */ 'DISCONNECTING': 4, + /** * The client has completed the connection and is no longer connected. + * + * @type {!number} */ - 'DISCONNECTED': 5, + 'DISCONNECTED': 5 + }; /** @@ -4059,6 +4096,17 @@ Guacamole.Display = function() { */ this.cursorY = 0; + /** + * The number of milliseconds over which display rendering statistics + * should be gathered, dispatching {@link #onstatistics} events as those + * statistics are available. If set to zero, no statistics will be + * gathered. + * + * @default 0 + * @type {!number} + */ + this.statisticWindow = 0; + /** * Fired when the default layer (and thus the entire Guacamole display) * is resized. @@ -4089,6 +4137,18 @@ Guacamole.Display = function() { */ this.oncursor = null; + /** + * Fired whenever performance statistics are available for recently- + * rendered frames. This event will fire only if {@link #statisticWindow} + * is non-zero. + * + * @event + * @param {!Guacamole.Display.Statistics} stats + * An object containing general rendering performance statistics for + * the remote desktop, Guacamole server, and Guacamole client. + */ + this.onstatistics = null; + /** * The queue of all pending Tasks. Tasks will be run in order, with new * tasks added at the end of the queue and old tasks removed from the @@ -4110,11 +4170,20 @@ Guacamole.Display = function() { var frames = []; /** - * Flushes all pending frames. + * Flushes all pending frames synchronously. This function will block until + * all pending frames have rendered. If a frame is currently blocked by an + * asynchronous operation like an image load, this function will return + * after reaching that operation and the flush operation will + * automamtically resume after that operation completes. + * * @private */ - function __flush_frames() { + var syncFlush = function syncFlush() { + var localTimestamp = 0; + var remoteTimestamp = 0; + + var renderedLogicalFrames = 0; var rendered_frames = 0; // Draw all pending frames, if ready @@ -4125,6 +4194,10 @@ Guacamole.Display = function() { break; frame.flush(); + + localTimestamp = frame.localTimestamp; + remoteTimestamp = frame.remoteTimestamp; + renderedLogicalFrames += frame.logicalFrames; rendered_frames++; } @@ -4132,6 +4205,112 @@ Guacamole.Display = function() { // Remove rendered frames from array frames.splice(0, rendered_frames); + if(rendered_frames) + notifyFlushed(localTimestamp, remoteTimestamp, renderedLogicalFrames); + + }; + + /** + * Recently-gathered display render statistics, as made available by calls + * to notifyFlushed(). The contents of this array will be trimmed to + * contain only up to {@link #statisticWindow} milliseconds of statistics. + * + * @private + * @type {Guacamole.Display.Statistics[]} + */ + var statistics = []; + + /** + * Notifies that one or more frames have been successfully rendered + * (flushed) to the display. + * + * @private + * @param {!number} localTimestamp + * The local timestamp of the point in time at which the most recent, + * flushed frame was received by the display, in milliseconds since the + * Unix Epoch. + * + * @param {!number} remoteTimestamp + * The remote timestamp of sync instruction associated with the most + * recent, flushed frame received by the display. This timestamp is in + * milliseconds, but is arbitrary, having meaning only relative to + * other timestamps in the same connection. + * + * @param {!number} logicalFrames + * The number of remote desktop frames that were flushed. + */ + var notifyFlushed = function notifyFlushed(localTimestamp, remoteTimestamp, logicalFrames) { + + // Ignore if statistics are not being gathered + if(!guac_display.statisticWindow) + return; + + var current = new Date().getTime(); + + // Find the first statistic that is still within the configured time + // window + for (var first = 0; first < statistics.length; first++) { + if(current - statistics[first].timestamp <= guac_display.statisticWindow) + break; + } + + // Remove all statistics except those within the time window + statistics.splice(0, first - 1); + + // Record statistics for latest frame + statistics.push({ + localTimestamp: localTimestamp, + remoteTimestamp: remoteTimestamp, + timestamp: current, + frames: logicalFrames + }); + + // Determine the actual time interval of the available statistics (this + // will not perfectly match the configured interval, which is an upper + // bound) + var statDuration = (statistics[statistics.length - 1].timestamp - statistics[0].timestamp) / 1000; + + // Determine the amount of time that elapsed remotely (within the + // remote desktop) + var remoteDuration = (statistics[statistics.length - 1].remoteTimestamp - statistics[0].remoteTimestamp) / 1000; + + // Calculate the number of frames that have been rendered locally + // within the configured time interval + var localFrames = statistics.length; + + // Calculate the number of frames actually received from the remote + // desktop by the Guacamole server + var remoteFrames = statistics.reduce(function sumFrames(prev, stat) { + return prev + stat.frames; + }, 0); + + // Calculate the number of frames that the Guacamole server had to + // drop or combine with other frames + var drops = statistics.reduce(function sumDrops(prev, stat) { + return prev + Math.max(0, stat.frames - 1); + }, 0); + + // Produce lag and FPS statistics from above raw measurements + var stats = new Guacamole.Display.Statistics({ + processingLag: current - localTimestamp, + desktopFps: (remoteDuration && remoteFrames) ? remoteFrames / remoteDuration : null, + clientFps: statDuration ? localFrames / statDuration : null, + serverFps: remoteDuration ? localFrames / remoteDuration : null, + dropRate: remoteDuration ? drops / remoteDuration : null + }); + + // Notify of availability of new statistics + if(guac_display.onstatistics) + guac_display.onstatistics(stats); + + }; + + /** + * Flushes all pending frames. + * @private + */ + function __flush_frames() { + syncFlush(); } /** @@ -4145,8 +4324,43 @@ Guacamole.Display = function() { * * @param {!Task[]} tasks * The set of tasks which must be executed to render this frame. + * + * @param {number} [timestamp] + * The remote timestamp of sync instruction associated with this frame. + * This timestamp is in milliseconds, but is arbitrary, having meaning + * only relative to other remote timestamps in the same connection. If + * omitted, a compatible but local timestamp will be used instead. + * + * @param {number} [logicalFrames=0] + * The number of remote desktop frames that were combined to produce + * this frame, or zero if this value is unknown or inapplicable. */ - function Frame(callback, tasks) { + var Frame = function Frame(callback, tasks, timestamp, logicalFrames) { + + /** + * The local timestamp of the point in time at which this frame was + * received by the display, in milliseconds since the Unix Epoch. + * + * @type {!number} + */ + this.localTimestamp = new Date().getTime(); + + /** + * The remote timestamp of sync instruction associated with this frame. + * This timestamp is in milliseconds, but is arbitrary, having meaning + * only relative to other remote timestamps in the same connection. + * + * @type {!number} + */ + this.remoteTimestamp = timestamp || this.localTimestamp; + + /** + * The number of remote desktop frames that were combined to produce + * this frame. If unknown or not applicable, this will be zero. + * + * @type {!number} + */ + this.logicalFrames = logicalFrames || 0; /** * Cancels rendering of this frame and all associated tasks. The @@ -4201,7 +4415,7 @@ Guacamole.Display = function() { }; - } + }; /** * A container for an task handler. Each operation which must be ordered @@ -4250,7 +4464,10 @@ Guacamole.Display = function() { this.unblock = function() { if(task.blocked) { task.blocked = false; - __flush_frames(); + + if(frames.length) + __flush_frames(); + } }; @@ -4378,11 +4595,20 @@ Guacamole.Display = function() { * @param {function} [callback] * The function to call when this frame is flushed. This may happen * immediately, or later when blocked tasks become unblocked. + * + * @param {number} timestamp + * The remote timestamp of sync instruction associated with this frame. + * This timestamp is in milliseconds, but is arbitrary, having meaning + * only relative to other remote timestamps in the same connection. + * + * @param {number} logicalFrames + * The number of remote desktop frames that were combined to produce + * this frame. */ - this.flush = function(callback) { + this.flush = function(callback, timestamp, logicalFrames) { // Add frame, reset tasks - frames.push(new Frame(callback, tasks)); + frames.push(new Frame(callback, tasks, timestamp, logicalFrames)); tasks = []; // Attempt flush @@ -4666,17 +4892,38 @@ Guacamole.Display = function() { */ this.drawStream = function drawStream(layer, x, y, stream, mimetype) { - // If createImageBitmap() is available, load the image as a blob so - // that function can be used - if(window.createImageBitmap) { - var reader = new Guacamole.BlobReader(stream, mimetype); - reader.onend = function drawImageBlob() { - guac_display.drawBlob(layer, x, y, reader.getBlob()); - }; + // Leverage ImageDecoder to decode the image stream as it is received + // whenever possible, as this reduces latency that might otherwise be + // caused by waiting for the full image to be received + if(window.ImageDecoder && window.ReadableStream) { + + var imageDecoder = new ImageDecoder({ + type: mimetype, + data: stream.toReadableStream() + }); + + var decodedFrame = null; + + // Draw image once loaded + var task = scheduleTask(function drawImageBitmap() { + layer.drawImage(x, y, decodedFrame); + }, true); + + imageDecoder.decode({ completeFramesOnly: true }).then(function bitmapLoaded(result) { + decodedFrame = result.image; + task.unblock(); + }); + } - // Lacking createImageBitmap(), fall back to data URIs and the Image - // object + // NOTE: We do not use Blobs and createImageBitmap() here, as doing so + // is very latent compared to the old data URI method and the new + // ImageDecoder object. The new ImageDecoder object is currently + // supported by most browsers, with other browsers being much faster if + // data URIs are used. The iOS version of Safari is particularly laggy + // if Blobs and createImageBitmap() are used instead. + + // Lacking ImageDecoder, fall back to data URIs and the Image object else { var reader = new Guacamole.DataURIReader(stream, mimetype); reader.onend = function drawImageDataURI() { @@ -5803,6 +6050,82 @@ Guacamole.Display.VisibleLayer = function(width, height) { */ Guacamole.Display.VisibleLayer.__next_id = 0; +/** + * A set of Guacamole display performance statistics, describing the speed at + * which the remote desktop, Guacamole server, and Guacamole client are + * rendering frames. + * + * @constructor + * @param {Guacamole.Display.Statistics|Object} [template={}] + * The object whose properties should be copied within the new + * Guacamole.Display.Statistics. + */ +Guacamole.Display.Statistics = function Statistics(template) { + + template = template || {}; + + /** + * The amount of time that the Guacamole client is taking to render + * individual frames, in milliseconds, if known. If this value is unknown, + * such as if the there are insufficient frame statistics recorded to + * calculate this value, this will be null. + * + * @type {?number} + */ + this.processingLag = template.processingLag; + + /** + * The framerate of the remote desktop currently being viewed within the + * relevant Gucamole.Display, independent of Guacamole, in frames per + * second. This represents the speed at which the remote desktop is + * producing frame data for the Guacamole server to consume. If this + * value is unknown, such as if the remote desktop server does not actually + * define frame boundaries, this will be null. + * + * @type {?number} + */ + this.desktopFps = template.desktopFps; + + /** + * The rate at which the Guacamole server is generating frames for the + * Guacamole client to consume, in frames per second. If the Guacamole + * server is correctly adjusting for variance in client/browser processing + * power, this rate should closely match the client rate, and should remain + * independent of any network latency. If this value is unknown, such as if + * the there are insufficient frame statistics recorded to calculate this + * value, this will be null. + * + * @type {?number} + */ + this.serverFps = template.serverFps; + + /** + * The rate at which the Guacamole client is consuming frames generated by + * the Guacamole server, in frames per second. If the Guacamole server is + * correctly adjusting for variance in client/browser processing power, + * this rate should closely match the server rate, regardless of any + * latency on the network between the server and client. If this value is + * unknown, such as if the there are insufficient frame statistics recorded + * to calculate this value, this will be null. + * + * @type {?number} + */ + this.clientFps = template.clientFps; + + /** + * The rate at which the Guacamole server is dropping or combining frames + * received from the remote desktop server to compensate for variance in + * client/browser processing power, in frames per second. This value may + * also be non-zero if the server is compensating for variances in its own + * processing power, or relative slowness in image compression vs. the rate + * that inbound frames are received. If this value is unknown, such as if + * the remote desktop server does not actually define frame boundaries, + * this will be null. + */ + this.dropRate = template.dropRate; + +}; + /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file @@ -6338,6 +6661,66 @@ Guacamole.InputStream = function(client, index) { client.sendAck(guac_stream.index, message, code); }; + /** + * Creates a new ReadableStream that receives the data sent to this stream + * by the Guacamole server. This function may be invoked at most once per + * stream, and invoking this function will overwrite any installed event + * handlers on this stream. + * + * A ReadableStream is a JavaScript object defined by the "Streams" + * standard. It is supported by most browsers, but not necessarily all + * browsers. The caller should verify this support is present before + * invoking this function. The behavior of this function when the browser + * does not support ReadableStream is not defined. + * + * @see {@link https://streams.spec.whatwg.org/#rs-class} + * + * @returns {!ReadableStream} + * A new ReadableStream that receives the bytes sent along this stream + * by the Guacamole server. + */ + this.toReadableStream = function toReadableStream() { + return new ReadableStream({ + type: 'bytes', + start: function startStream(controller) { + + var reader = new Guacamole.ArrayBufferReader(guac_stream); + + // Provide any received blocks of data to the ReadableStream + // controller, such that they will be read by whatever is + // consuming the ReadableStream + reader.ondata = function dataReceived(data) { + + if(controller.byobRequest) { + + var view = controller.byobRequest.view; + var length = Math.min(view.byteLength, data.byteLength); + var byobBlock = new Uint8Array(data, 0, length); + + view.buffer.set(byobBlock); + controller.byobRequest.respond(length); + + if(length < data.byteLength) { + controller.enqueue(data.slice(length)); + } + + } else { + controller.enqueue(new Uint8Array(data)); + } + + }; + + // Notify the ReadableStream when the end of the stream is + // reached + reader.onend = function dataComplete() { + controller.close(); + }; + + } + }); + + }; + }; /* @@ -6832,6 +7215,10 @@ Guacamole.Keyboard = function Keyboard(element) { // Determine whether default action for Alt+combinations must be prevented var prevent_alt = !this.modifiers.ctrl && !quirks.altIsTypableOnly; + // If alt is typeable only, and this is actually an alt key event, treat as AltGr instead + if(quirks.altIsTypableOnly && (this.keysym === 0xFFE9 || this.keysym === 0xFFEA)) + this.keysym = 0xFE03; + // Determine whether default action for Ctrl+combinations must be prevented var prevent_ctrl = !this.modifiers.alt; @@ -6932,7 +7319,7 @@ Guacamole.Keyboard = function Keyboard(element) { 13: [0xFF0D], // enter 16: [0xFFE1, 0xFFE1, 0xFFE2], // shift 17: [0xFFE3, 0xFFE3, 0xFFE4], // ctrl - 18: [0xFFE9, 0xFFE9, 0xFE03], // alt + 18: [0xFFE9, 0xFFE9, 0xFFEA], // alt 19: [0xFF13], // pause/break 20: [0xFFE5], // caps lock 27: [0xFF1B], // escape @@ -6993,7 +7380,7 @@ Guacamole.Keyboard = function Keyboard(element) { 'Again': [0xFF66], 'AllCandidates': [0xFF3D], 'Alphanumeric': [0xFF30], - 'Alt': [0xFFE9, 0xFFE9, 0xFE03], + 'Alt': [0xFFE9, 0xFFE9, 0xFFEA], 'Attn': [0xFD0E], 'AltGraph': [0xFE03], 'ArrowDown': [0xFF54], @@ -7004,7 +7391,7 @@ Guacamole.Keyboard = function Keyboard(element) { 'CapsLock': [0xFFE5], 'Cancel': [0xFF69], 'Clear': [0xFF0B], - 'Convert': [0xFF21], + 'Convert': [0xFF23], 'Copy': [0xFD15], 'Crsel': [0xFD1C], 'CrSel': [0xFD1C], @@ -7071,6 +7458,7 @@ Guacamole.Keyboard = function Keyboard(element) { 'Left': [0xFF51], 'Meta': [0xFFE7, 0xFFE7, 0xFFE8], 'ModeChange': [0xFF7E], + 'NonConvert': [0xFF22], 'NumLock': [0xFF7F], 'PageDown': [0xFF56], 'PageUp': [0xFF55], @@ -7080,6 +7468,7 @@ Guacamole.Keyboard = function Keyboard(element) { 'PrintScreen': [0xFF61], 'Redo': [0xFF66], 'Right': [0xFF53], + 'Romaji': [0xFF24], 'RomanCharacters': null, 'Scroll': [0xFF14], 'Select': [0xFF60], @@ -7851,9 +8240,10 @@ Guacamole.Keyboard = function Keyboard(element) { var keydownEvent = new KeydownEvent(e); - // Ignore (but do not prevent) the "composition" keycode sent by some - // browsers when an IME is in use (see: http://lists.w3.org/Archives/Public/www-dom/2010JulSep/att-0182/keyCode-spec.html) - if(keydownEvent.keyCode === 229) + // Ignore (but do not prevent) the event if explicitly marked as composing, + // or when the "composition" keycode sent by some browsers when an IME is in use + // (see: http://lists.w3.org/Archives/Public/www-dom/2010JulSep/att-0182/keyCode-spec.html) + if(e.isComposing || keydownEvent.keyCode === 229) return; // Log event @@ -7902,8 +8292,6 @@ Guacamole.Keyboard = function Keyboard(element) { /** * Handles the given "input" event, typing the data within the input text. - * If the event is complete (text is provided), handling of "compositionend" - * events is suspended, as such events may conflict with input events. * * @private * @param {!InputEvent} e @@ -7918,24 +8306,37 @@ Guacamole.Keyboard = function Keyboard(element) { if(!markEvent(e)) return; // Type all content written - if(e.data && !e.isComposing) { - element.removeEventListener('compositionend', handleComposition, false); + if(e.data && !e.isComposing) guac_keyboard.type(e.data); - } + + }; + + /** + * Handles the given "compositionstart" event, automatically removing + * the "input" event handler, as "input" events should only be handled + * if composition events are not provided by the browser. + * + * @private + * @param {!CompositionEvent} e + * The "compositionstart" event to handle. + */ + var handleCompositionStart = function handleCompositionStart(e) { + + // Remove the "input" event handler now that the browser is known + // to send composition events + element.removeEventListener('input', handleInput, false); }; /** * Handles the given "compositionend" event, typing the data within the - * composed text. If the event is complete (composed text is provided), - * handling of "input" events is suspended, as such events may conflict - * with composition events. + * composed text. * * @private * @param {!CompositionEvent} e * The "compositionend" event to handle. */ - var handleComposition = function handleComposition(e) { + var handleCompositionEnd = function handleCompositionEnd(e) { // Only intercept if handler set if(!guac_keyboard.onkeydown && !guac_keyboard.onkeyup) return; @@ -7944,16 +8345,15 @@ Guacamole.Keyboard = function Keyboard(element) { if(!markEvent(e)) return; // Type all content written - if(e.data) { - element.removeEventListener('input', handleInput, false); + if(e.data) guac_keyboard.type(e.data); - } }; // Automatically type text entered into the wrapped field element.addEventListener('input', handleInput, false); - element.addEventListener('compositionend', handleComposition, false); + element.addEventListener('compositionend', handleCompositionEnd, false); + element.addEventListener('compositionstart', handleCompositionStart, false); }; @@ -8067,6 +8467,343 @@ Guacamole.Keyboard.ModifierState.fromKeyboardEvent = function(e) { var Guacamole = Guacamole || {}; +/** + * An object that will accept raw key events and produce a chronologically + * ordered array of key event objects. These events can be obtained by + * calling getEvents(). + * + * @constructor + * @param {number} [startTimestamp=0] + * The starting timestamp for the recording being intepreted. If provided, + * the timestamp of each intepreted event will be relative to this timestamp. + * If not provided, the raw recording timestamp will be used. + */ +Guacamole.KeyEventInterpreter = function KeyEventInterpreter(startTimestamp) { + + // Default to 0 seconds to keep the raw timestamps + if(startTimestamp === undefined || startTimestamp === null) + startTimestamp = 0; + + /** + * A precursor array to the KNOWN_KEYS map. The objects contained within + * will be constructed into full KeyDefinition objects. + * + * @constant + * @private + * @type {Object[]} + */ + var _KNOWN_KEYS = [ + { keysym: 0xFE03, name: 'AltGr' }, + { keysym: 0xFF08, name: 'Backspace' }, + { keysym: 0xFF09, name: 'Tab' }, + { keysym: 0xFF0B, name: 'Clear' }, + { keysym: 0xFF0D, name: 'Return', value: '\n' }, + { keysym: 0xFF13, name: 'Pause' }, + { keysym: 0xFF14, name: 'Scroll' }, + { keysym: 0xFF15, name: 'SysReq' }, + { keysym: 0xFF1B, name: 'Escape' }, + { keysym: 0xFF50, name: 'Home' }, + { keysym: 0xFF51, name: 'Left' }, + { keysym: 0xFF52, name: 'Up' }, + { keysym: 0xFF53, name: 'Right' }, + { keysym: 0xFF54, name: 'Down' }, + { keysym: 0xFF55, name: 'Page Up' }, + { keysym: 0xFF56, name: 'Page Down' }, + { keysym: 0xFF57, name: 'End' }, + { keysym: 0xFF63, name: 'Insert' }, + { keysym: 0xFF65, name: 'Undo' }, + { keysym: 0xFF6A, name: 'Help' }, + { keysym: 0xFF7F, name: 'Num' }, + { keysym: 0xFF80, name: 'Space', value: ' ' }, + { keysym: 0xFF8D, name: 'Enter', value: '\n' }, + { keysym: 0xFF95, name: 'Home' }, + { keysym: 0xFF96, name: 'Left' }, + { keysym: 0xFF97, name: 'Up' }, + { keysym: 0xFF98, name: 'Right' }, + { keysym: 0xFF99, name: 'Down' }, + { keysym: 0xFF9A, name: 'Page Up' }, + { keysym: 0xFF9B, name: 'Page Down' }, + { keysym: 0xFF9C, name: 'End' }, + { keysym: 0xFF9E, name: 'Insert' }, + { keysym: 0xFFAA, name: '*', value: '*' }, + { keysym: 0xFFAB, name: '+', value: '+' }, + { keysym: 0xFFAD, name: '-', value: '-' }, + { keysym: 0xFFAE, name: '.', value: '.' }, + { keysym: 0xFFAF, name: '/', value: '/' }, + { keysym: 0xFFB0, name: '0', value: '0' }, + { keysym: 0xFFB1, name: '1', value: '1' }, + { keysym: 0xFFB2, name: '2', value: '2' }, + { keysym: 0xFFB3, name: '3', value: '3' }, + { keysym: 0xFFB4, name: '4', value: '4' }, + { keysym: 0xFFB5, name: '5', value: '5' }, + { keysym: 0xFFB6, name: '6', value: '6' }, + { keysym: 0xFFB7, name: '7', value: '7' }, + { keysym: 0xFFB8, name: '8', value: '8' }, + { keysym: 0xFFB9, name: '9', value: '9' }, + { keysym: 0xFFBE, name: 'F1' }, + { keysym: 0xFFBF, name: 'F2' }, + { keysym: 0xFFC0, name: 'F3' }, + { keysym: 0xFFC1, name: 'F4' }, + { keysym: 0xFFC2, name: 'F5' }, + { keysym: 0xFFC3, name: 'F6' }, + { keysym: 0xFFC4, name: 'F7' }, + { keysym: 0xFFC5, name: 'F8' }, + { keysym: 0xFFC6, name: 'F9' }, + { keysym: 0xFFC7, name: 'F10' }, + { keysym: 0xFFC8, name: 'F11' }, + { keysym: 0xFFC9, name: 'F12' }, + { keysym: 0xFFCA, name: 'F13' }, + { keysym: 0xFFCB, name: 'F14' }, + { keysym: 0xFFCC, name: 'F15' }, + { keysym: 0xFFCD, name: 'F16' }, + { keysym: 0xFFCE, name: 'F17' }, + { keysym: 0xFFCF, name: 'F18' }, + { keysym: 0xFFD0, name: 'F19' }, + { keysym: 0xFFD1, name: 'F20' }, + { keysym: 0xFFD2, name: 'F21' }, + { keysym: 0xFFD3, name: 'F22' }, + { keysym: 0xFFD4, name: 'F23' }, + { keysym: 0xFFD5, name: 'F24' }, + { keysym: 0xFFE1, name: 'Shift' }, + { keysym: 0xFFE2, name: 'Shift' }, + { keysym: 0xFFE3, name: 'Ctrl' }, + { keysym: 0xFFE4, name: 'Ctrl' }, + { keysym: 0xFFE5, name: 'Caps' }, + { keysym: 0xFFE7, name: 'Meta' }, + { keysym: 0xFFE8, name: 'Meta' }, + { keysym: 0xFFE9, name: 'Alt' }, + { keysym: 0xFFEA, name: 'Alt' }, + { keysym: 0xFFEB, name: 'Super' }, + { keysym: 0xFFEC, name: 'Super' }, + { keysym: 0xFFED, name: 'Hyper' }, + { keysym: 0xFFEE, name: 'Hyper' }, + { keysym: 0xFFFF, name: 'Delete' } + ]; + + /** + * All known keys, as a map of X11 keysym to KeyDefinition. + * + * @constant + * @private + * @type {Object.} + */ + var KNOWN_KEYS = {}; + _KNOWN_KEYS.forEach(function createKeyDefinitionMap(keyDefinition) { + + // Construct a map of keysym to KeyDefinition object + KNOWN_KEYS[keyDefinition.keysym] = ( + new Guacamole.KeyEventInterpreter.KeyDefinition(keyDefinition)); + + }); + + /** + * All key events parsed as of the most recent handleKeyEvent() invocation. + * + * @private + * @type {!Guacamole.KeyEventInterpreter.KeyEvent[]} + */ + var parsedEvents = []; + + /** + * If the provided keysym corresponds to a valid UTF-8 character, return + * a KeyDefinition for that keysym. Otherwise, return null. + * + * @private + * @param {Number} keysym + * The keysym to produce a UTF-8 KeyDefinition for, if valid. + * + * @returns {Guacamole.KeyEventInterpreter.KeyDefinition} + * A KeyDefinition for the provided keysym, if it's a valid UTF-8 + * keysym, or null otherwise. + */ + function getUnicodeKeyDefinition(keysym) { + + // Translate only if keysym maps to Unicode + if(keysym < 0x00 || (keysym > 0xFF && (keysym | 0xFFFF) != 0x0100FFFF)) + return null; + + // Convert to UTF8 string + var codepoint = keysym & 0xFFFF; + var name = String.fromCharCode(codepoint); + + // Create and return the definition + return new Guacamole.KeyEventInterpreter.KeyDefinition({ + keysym: keysym, name: name, value: name + }); + + } + + /** + * Return a KeyDefinition corresponding to the provided keysym. + * + * @private + * @param {Number} keysym + * The keysym to return a KeyDefinition for. + * + * @returns {KeyDefinition} + * A KeyDefinition corresponding to the provided keysym. + */ + function getKeyDefinitionByKeysym(keysym) { + + // If it's a known type, return the existing definition + if(keysym in KNOWN_KEYS) + return KNOWN_KEYS[keysym]; + + // Return a UTF-8 KeyDefinition, if valid + var definition = getUnicodeKeyDefinition(keysym); + if(definition != null) + return definition; + + // If it's not UTF-8, return an unknown definition, with the name + // just set to the hex value of the keysym + return new Guacamole.KeyEventInterpreter.KeyDefinition({ + keysym: keysym, + name: '0x' + String(keysym.toString(16)) + }); + + } + + /** + * Handles a raw key event, appending a new key event object for every + * handled raw event. + * + * @param {!string[]} args + * The arguments of the key event. + */ + this.handleKeyEvent = function handleKeyEvent(args) { + + // The X11 keysym + var keysym = parseInt(args[0]); + + // Either 1 or 0 for pressed or released, respectively + var pressed = parseInt(args[1]); + + // The timestamp when this key event occured + var timestamp = parseInt(args[2]); + + // The timestamp relative to the provided initial timestamp + var relativeTimestap = timestamp - startTimestamp; + + // Known information about the parsed key + var definition = getKeyDefinitionByKeysym(keysym); + + // Push the latest parsed event into the list + parsedEvents.push(new Guacamole.KeyEventInterpreter.KeyEvent({ + definition: definition, + pressed: pressed, + timestamp: relativeTimestap + })); + + }; + + /** + * Return the current batch of typed text. Note that the batch may be + * incomplete, as more key events might be processed before the next + * batch starts. + * + * @returns {Guacamole.KeyEventInterpreter.KeyEvent[]} + * The current batch of text. + */ + this.getEvents = function getEvents() { + return parsedEvents; + }; + +}; + +/** + * A definition for a known key. + * + * @constructor + * @param {Guacamole.KeyEventInterpreter.KeyDefinition|object} [template={}] + * The object whose properties should be copied within the new + * KeyDefinition. + */ +Guacamole.KeyEventInterpreter.KeyDefinition = function KeyDefinition(template) { + + // Use empty object by default + template = template || {}; + + /** + * The X11 keysym of the key. + * @type {!number} + */ + this.keysym = parseInt(template.keysym); + + /** + * A human-readable name for the key. + * @type {!String} + */ + this.name = template.name; + + /** + * The value which would be typed in a typical text editor, if any. If the + * key is not associated with any typeable value, this will be undefined. + * @type {String} + */ + this.value = template.value; + +}; + +/** + * A granular description of an extracted key event, including a human-readable + * text representation of the event, whether the event is directly typed or not, + * and the timestamp when the event occured. + * + * @constructor + * @param {Guacamole.KeyEventInterpreter.KeyEvent|object} [template={}] + * The object whose properties should be copied within the new + * KeyEvent. + */ +Guacamole.KeyEventInterpreter.KeyEvent = function KeyEvent(template) { + + // Use empty object by default + template = template || {}; + + /** + * The key definition for the pressed key. + * + * @type {!Guacamole.KeyEventInterpreter.KeyDefinition} + */ + this.definition = template.definition; + + /** + * True if the key was pressed to create this event, or false if it was + * released. + * + * @type {!boolean} + */ + this.pressed = !!template.pressed; + + /** + * The timestamp from the recording when this event occured. + * + * @type {!Number} + */ + this.timestamp = template.timestamp; + +}; + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +var Guacamole = Guacamole || {}; + /** * Abstract ordered drawing surface. Each Layer contains a canvas element and * provides simple drawing instructions for drawing to that canvas element, @@ -9461,9 +10198,15 @@ Guacamole.Mouse = function Mouse(element) { } - element.addEventListener('DOMMouseScroll', mousewheel_handler, false); - element.addEventListener('mousewheel', mousewheel_handler, false); - element.addEventListener('wheel', mousewheel_handler, false); + if(window.WheelEvent) { + // All modern browsers support wheel events. + element.addEventListener('wheel', mousewheel_handler, false); + } else { + // Legacy FireFox wheel events. + element.addEventListener('DOMMouseScroll', mousewheel_handler, false); + // Legacy Chrome/IE/other wheel events. + element.addEventListener('mousewheel', mousewheel_handler, false); + } /** * Whether the browser supports CSS3 cursor styling, including hotspot @@ -12377,8 +13120,18 @@ var Guacamole = Guacamole || {}; * @param {!Blob|Guacamole.Tunnel} source * The Blob from which the instructions of the recording should * be read. + * @param {number} [refreshInterval=1000] + * The minimum number of milliseconds between updates to the recording + * position through the provided onseek() callback. If non-positive, this + * parameter will be ignored, and the recording position will only be + * updated when seek requests are made, or when new frames are rendered. + * If not specified, refreshInterval will default to 1000 milliseconds. */ -Guacamole.SessionRecording = function SessionRecording(source) { +Guacamole.SessionRecording = function SessionRecording(source, refreshInterval) { + + // Default the refresh interval to 1 second if not specified otherwise + if(refreshInterval === undefined) + refreshInterval = 1000; /** * Reference to this Guacamole.SessionRecording. @@ -12484,13 +13237,13 @@ Guacamole.SessionRecording = function SessionRecording(source) { var currentFrame = -1; /** - * The timestamp of the frame when playback began, in milliseconds. If + * The position of the recording when playback began, in milliseconds. If * playback is not in progress, this will be null. * * @private * @type {number} */ - var startVideoTimestamp = null; + var startVideoPosition = null; /** * The real-world timestamp when playback began, in milliseconds. If @@ -12501,6 +13254,14 @@ Guacamole.SessionRecording = function SessionRecording(source) { */ var startRealTimestamp = null; + /** + * The current position within the recording, in milliseconds. + * + * @private + * @type {!number} + */ + var currentPosition = 0; + /** * An object containing a single "aborted" property which is set to * true if the in-progress seek operation should be aborted. If no seek @@ -12556,6 +13317,25 @@ Guacamole.SessionRecording = function SessionRecording(source) { */ var seekCallback = null; + /** + * Any current timeout associated with scheduling frame replay, or updating + * the current position, or null if no frame position increment is currently + * scheduled. + * + * @private + * @type {number} + */ + var updateTimeout = null; + + /** + * The browser timestamp of the last time that currentPosition was updated + * while playing, or null if the recording is not currently playing. + * + * @private + * @type {number} + */ + var lastUpdateTimestamp = null; + /** * Parses all Guacamole instructions within the given blob, invoking * the provided instruction callback for each such instruction. Once @@ -12682,6 +13462,28 @@ Guacamole.SessionRecording = function SessionRecording(source) { // Hide cursor unless mouse position is received playbackClient.getDisplay().showCursor(false); + /** + * A key event interpreter to split all key events in this recording into + * human-readable batches of text. Constrcution is deferred until the first + * event is processed, to enable recording-relative timestamps. + * + * @type {!Guacamole.KeyEventInterpreter} + */ + var keyEventInterpreter = null; + + /** + * Initialize the key interpreter. This function should be called only once + * with the first timestamp in the recording as an argument. + * + * @private + * @param {!number} startTimestamp + * The timestamp of the first frame in the recording, i.e. the start of + * the recording. + */ + function initializeKeyInterpreter(startTimestamp) { + keyEventInterpreter = new Guacamole.KeyEventInterpreter(startTimestamp); + } + /** * Handles a newly-received instruction, whether from the main Blob or a * tunnel, adding new frames and keyframes as necessary. Load progress is @@ -12713,6 +13515,11 @@ Guacamole.SessionRecording = function SessionRecording(source) { frames.push(frame); frameStart = frameEnd; + // If this is the first frame, intialize the key event interpreter + // with the timestamp of the first frame + if(frames.length === 1) + initializeKeyInterpreter(timestamp); + // This frame should eventually become a keyframe if enough data // has been processed and enough recording time has elapsed, or if // this is the absolute first frame @@ -12720,14 +13527,15 @@ Guacamole.SessionRecording = function SessionRecording(source) { && timestamp - frames[lastKeyframe].timestamp >= KEYFRAME_TIME_INTERVAL)) { frame.keyframe = true; lastKeyframe = frames.length - 1; + } // Notify that additional content is available if(recording.onprogress) recording.onprogress(recording.getDuration(), frameEnd); - } - + } else if(opcode === 'key') + keyEventInterpreter.handleKeyEvent(args); }; /** @@ -12794,6 +13602,11 @@ Guacamole.SessionRecording = function SessionRecording(source) { instructionBuffer = ''; } + // Now that the recording is fully processed, and all key events + // have been extracted, call the onkeyevents handler if defined + if(recording.onkeyevents) + recording.onkeyevents(keyEventInterpreter.getEvents()); + // Consider recording loaded if tunnel has closed without errors if(!errorEncountered) notifyLoaded(); @@ -12826,8 +13639,9 @@ Guacamole.SessionRecording = function SessionRecording(source) { }; /** - * Searches through the given region of frames for the frame having a - * relative timestamp closest to the timestamp given. + * Searches through the given region of frames for the closest frame + * having a relative timestamp less than or equal to the to the given + * relative timestamp. * * @private * @param {!number} minIndex @@ -12848,9 +13662,22 @@ Guacamole.SessionRecording = function SessionRecording(source) { */ var findFrame = function findFrame(minIndex, maxIndex, timestamp) { - // Do not search if the region contains only one element - if(minIndex === maxIndex) - return minIndex; + // The region has only one frame - determine if it is before or after + // the requested timestamp + if(minIndex === maxIndex) { + + // Skip checking if this is the very first frame - no frame could + // possibly be earlier + if(minIndex === 0) + return minIndex; + + // If the closest frame occured after the requested timestamp, + // return the previous frame, which will be the closest with a + // timestamp before the requested timestamp + if(toRelativeTimestamp(frames[minIndex].timestamp) > timestamp) + return minIndex - 1; + + } // Split search region into two halves var midIndex = Math.floor((minIndex + maxIndex) / 2); @@ -12943,10 +13770,11 @@ Guacamole.SessionRecording = function SessionRecording(source) { // Replay any applicable incremental frames var continueReplay = function continueReplay() { - // Notify of changes in position + // Set the current position and notify changes if(recording.onseek && currentFrame > startIndex) { - recording.onseek(toRelativeTimestamp(frames[currentFrame].timestamp), - currentFrame - startIndex, index - startIndex); + currentPosition = toRelativeTimestamp(frames[currentFrame].timestamp); + recording.onseek(currentPosition, currentFrame - startIndex, + index - startIndex); } // Cancel seek if aborted @@ -12967,9 +13795,18 @@ Guacamole.SessionRecording = function SessionRecording(source) { // immediately if no delay was requested var continueAfterRequiredDelay = function continueAfterRequiredDelay() { var delay = nextRealTimestamp ? Math.max(nextRealTimestamp - new Date().getTime(), 0) : 0; - if(delay) - window.setTimeout(continueReplay, delay); - else + if(delay) { + + // Clear any already-scheduled update before scheduling again + // to avoid multiple updates in flight at the same time + updateTimeout && clearTimeout(updateTimeout); + + // Schedule with the appropriate delay + updateTimeout = window.setTimeout(function timeoutComplete() { + updateTimeout = null; + continueReplay(); + }, delay); + } else continueReplay(); }; @@ -13022,20 +13859,73 @@ Guacamole.SessionRecording = function SessionRecording(source) { */ var continuePlayback = function continuePlayback() { + // Do not continue playback if the recording is paused + if(!recording.isPlaying()) + return; + // If frames remain after advancing, schedule next frame if(currentFrame + 1 < frames.length) { // Pull the upcoming frame var next = frames[currentFrame + 1]; - // Calculate the real timestamp corresponding to when the next - // frame begins - var nextRealTimestamp = next.timestamp - startVideoTimestamp + startRealTimestamp; + // The number of elapsed milliseconds on the clock since playback began + var realLifePlayTime = Date.now() - startRealTimestamp; - // Advance to next frame after enough time has elapsed - seekToFrame(currentFrame + 1, function frameDelayElapsed() { - continuePlayback(); - }, nextRealTimestamp); + // The number of milliseconds between the recording position when + // playback started and the position of the next frame + var timestampOffset = ( + toRelativeTimestamp(next.timestamp) - startVideoPosition); + + // The delay until the next frame should be rendered, taking into + // account any accumulated delays from rendering frames so far + var nextFrameDelay = timestampOffset - realLifePlayTime; + + // The delay until the refresh interval would induce an update to + // the current recording position, rounded to the nearest whole + // multiple of refreshInterval to ensure consistent timing for + // refresh intervals even with inconsistent frame timing + var nextRefreshDelay = refreshInterval >= 0 + ? (refreshInterval * (Math.floor( + (currentPosition + refreshInterval) / refreshInterval)) + ) - currentPosition + : nextFrameDelay; + + // If the next frame will occur before the next refresh interval, + // advance to the frame after the appropriate delay + if(nextFrameDelay <= nextRefreshDelay) + + seekToFrame(currentFrame + 1, function frameDelayElapsed() { + + // Record when the timestamp was updated and continue on + lastUpdateTimestamp = Date.now(); + continuePlayback(); + + }, Date.now() + nextFrameDelay); + + // The position needs to be incremented before the next frame + else { + + // Clear any existing update timeout + updateTimeout && window.clearTimeout(updateTimeout); + + updateTimeout = window.setTimeout(function incrementPosition() { + + updateTimeout = null; + + // Update the position + currentPosition += nextRefreshDelay; + + // Notifiy the new position using the onseek handler + if(recording.onseek) + recording.onseek(currentPosition); + + // Record when the timestamp was updated and continue on + lastUpdateTimestamp = Date.now(); + continuePlayback(); + + }, nextRefreshDelay); + } } @@ -13101,6 +13991,17 @@ Guacamole.SessionRecording = function SessionRecording(source) { */ this.onpause = null; + /** + * Fired with all extracted key events when the recording is fully + * processed. The callback will be invoked with an empty list + * if no key events were extracted. + * + * @event + * @param {!Guacamole.KeyEventInterpreter.KeyEvent[]} batch + * The extracted key events. + */ + this.onkeyevents = null; + /** * Fired whenever the playback position within the recording changes. * @@ -13183,7 +14084,7 @@ Guacamole.SessionRecording = function SessionRecording(source) { * true if playback is currently in progress, false otherwise. */ this.isPlaying = function isPlaying() { - return !!startVideoTimestamp; + return !!startRealTimestamp; }; /** @@ -13195,13 +14096,7 @@ Guacamole.SessionRecording = function SessionRecording(source) { */ this.getPosition = function getPosition() { - // Position is simply zero if playback has not started at all - if(currentFrame === -1) - return 0; - - // Return current position as a millisecond timestamp relative to the - // start of the recording - return toRelativeTimestamp(frames[currentFrame].timestamp); + return currentPosition; }; @@ -13245,11 +14140,11 @@ Guacamole.SessionRecording = function SessionRecording(source) { // Store timestamp of playback start for relative scheduling of // future frames - var next = frames[currentFrame + 1]; - startVideoTimestamp = next.timestamp; - startRealTimestamp = new Date().getTime(); + startVideoPosition = currentPosition; + startRealTimestamp = Date.now(); // Begin playback of video + lastUpdateTimestamp = Date.now(); continuePlayback(); } @@ -13301,8 +14196,23 @@ Guacamole.SessionRecording = function SessionRecording(source) { }; - // Perform seek - seekToFrame(findFrame(0, frames.length - 1, position), seekCallback); + // Find the index of the closest frame at or before the requested position + var closestFrame = findFrame(0, frames.length - 1, position); + + // Seek to the closest frame before or at the requested position + seekToFrame(closestFrame, function seekComplete() { + + // Update the current position to the requested position + // and invoke the the onseek callback. Note that this is the + // position provided to this function, NOT the position of the + // frame that was just seeked + currentPosition = position; + if(recording.onseek) + recording.onseek(position); + + seekCallback(); + + }); }; @@ -13332,6 +14242,13 @@ Guacamole.SessionRecording = function SessionRecording(source) { // Abort any in-progress seek / playback abortSeek(); + // Cancel any currently-scheduled updates + updateTimeout && clearTimeout(updateTimeout); + + // Increment the current position by the amount of time passed since the + // the last time it was updated + currentPosition += Date.now() - lastUpdateTimestamp; + // Stop playback only if playback is in progress if(recording.isPlaying()) { @@ -13340,7 +14257,8 @@ Guacamole.SessionRecording = function SessionRecording(source) { recording.onpause(); // Playback is stopped - startVideoTimestamp = null; + lastUpdateTimestamp = null; + startVideoPosition = null; startRealTimestamp = null; } @@ -13469,6 +14387,7 @@ Guacamole.SessionRecording._PlaybackTunnel = function _PlaybackTunnel() { }; }; + /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file @@ -15907,7 +16826,7 @@ var Guacamole = Guacamole || {}; * * @type {!string} */ -Guacamole.API_VERSION = '1.5.4'; +Guacamole.API_VERSION = '1.6.0'; /* * Licensed to the Apache Software Foundation (ASF) under one diff --git a/orion-visor-ui/package.json b/orion-visor-ui/package.json index 8832e820..25609244 100644 --- a/orion-visor-ui/package.json +++ b/orion-visor-ui/package.json @@ -1,7 +1,7 @@ { "name": "orion-visor-ui", "description": "Orion Visor UI", - "version": "2.4.0", + "version": "2.4.1", "private": true, "author": "Jiahang Li", "license": "Apache 2.0", diff --git a/orion-visor-ui/src/api/asset/host-extra.ts b/orion-visor-ui/src/api/asset/host-extra.ts index 564d47e2..9fb2b142 100644 --- a/orion-visor-ui/src/api/asset/host-extra.ts +++ b/orion-visor-ui/src/api/asset/host-extra.ts @@ -30,6 +30,7 @@ export interface HostRdpExtraSettingModel { authType: string; identityId: number; lowBandwidthMode: boolean; + initialProgram: string; } // 标签额外配置 diff --git a/orion-visor-ui/src/api/system/setting.ts b/orion-visor-ui/src/api/system/setting.ts index b193c956..2c9b3234 100644 --- a/orion-visor-ui/src/api/system/setting.ts +++ b/orion-visor-ui/src/api/system/setting.ts @@ -15,7 +15,6 @@ export interface SystemSettingUpdateRequest { */ export interface AppInfoResponse { version: string; - uuid: string; } /** diff --git a/orion-visor-ui/src/api/terminal/terminal-file-log.ts b/orion-visor-ui/src/api/terminal/terminal-file-log.ts new file mode 100644 index 00000000..9a73047b --- /dev/null +++ b/orion-visor-ui/src/api/terminal/terminal-file-log.ts @@ -0,0 +1,70 @@ +import type { DataGrid, OrderDirection, Pagination } from '@/types/global'; +import type { TableData } from '@arco-design/web-vue'; +import axios from 'axios'; +import qs from 'query-string'; + +/** + * 终端文件操作日志 查询请求 + */ +export interface TerminalFileLogQueryRequest extends Pagination, OrderDirection { + userId?: number; + hostId?: number; + type?: string; + result?: number; + startTimeRange?: string[]; +} + +/** + * 终端文件操作日志 查询响应 + */ +export interface TerminalFileLogQueryResponse extends TableData { + id: number; + userId: number; + username: number; + hostId: number; + hostName: string; + hostAddress: string; + address: string; + location: string; + userAgent: string; + paths: string[]; + type: string; + result: string; + startTime: number; + extra: TerminalFileLogExtra; +} + +/** + * 终端文件操作日志 拓展对象 + */ +export interface TerminalFileLogExtra { + mod: number; + target: string; + maxCount: number; +} + +/** + * 分页查询终端文件操作日志 + */ +export function getTerminalFileLogPage(request: TerminalFileLogQueryRequest) { + return axios.post>('/terminal/terminal-file-log/query', request); +} + +/** + * 查询终端文件操作日志数量 + */ +export function getTerminalFileLogCount(request: TerminalFileLogQueryRequest) { + return axios.post('/terminal/terminal-file-log/count', request); +} + +/** + * 删除终端文件操作日志 + */ +export function deleteTerminalFileLog(idList: Array) { + return axios.delete('/terminal/terminal-file-log/delete', { + params: { idList }, + paramsSerializer: params => { + return qs.stringify(params, { arrayFormat: 'comma' }); + } + }); +} diff --git a/orion-visor-ui/src/api/terminal/terminal-sftp.ts b/orion-visor-ui/src/api/terminal/terminal-sftp.ts index 07b43053..43f837fd 100644 --- a/orion-visor-ui/src/api/terminal/terminal-sftp.ts +++ b/orion-visor-ui/src/api/terminal/terminal-sftp.ts @@ -1,74 +1,5 @@ -import type { DataGrid, OrderDirection, Pagination } from '@/types/global'; -import type { TableData } from '@arco-design/web-vue'; import { httpBaseUrl } from '@/utils/env'; import axios from 'axios'; -import qs from 'query-string'; - -/** - * SFTP 操作日志 查询请求 - */ -export interface TerminalSftpLogQueryRequest extends Pagination, OrderDirection { - userId?: number; - hostId?: number; - type?: string; - result?: number; - startTimeRange?: string[]; -} - -/** - * SFTP 操作日志 查询响应 - */ -export interface TerminalSftpLogQueryResponse extends TableData { - id: number; - userId: number; - username: number; - hostId: number; - hostName: string; - hostAddress: string; - address: string; - location: string; - userAgent: string; - paths: string[]; - type: string; - result: string; - startTime: number; - extra: TerminalSftpLogExtra; -} - -/** - * SFTP 操作日志 拓展对象 - */ -export interface TerminalSftpLogExtra { - mod: number; - target: string; - maxCount: number; -} - -/** - * 分页查询 SFTP 操作日志 - */ -export function getTerminalSftpLogPage(request: TerminalSftpLogQueryRequest) { - return axios.post>('/terminal/terminal-sftp/query-log', request); -} - -/** - * 查询 SFTP 操作日志数量 - */ -export function getTerminalSftpLogCount(request: TerminalSftpLogQueryRequest) { - return axios.post('/terminal/terminal-sftp/log-count', request); -} - -/** - * 删除 SFTP 操作日志 - */ -export function deleteTerminalSftpLog(idList: Array) { - return axios.delete('/terminal/terminal-sftp/delete-log', { - params: { idList }, - paramsSerializer: params => { - return qs.stringify(params, { arrayFormat: 'comma' }); - } - }); -} /** * 获取 SFTP 文件内容 diff --git a/orion-visor-ui/src/api/user/user-aggregate.ts b/orion-visor-ui/src/api/user/user-aggregate.ts index 3e274670..fa95ac6f 100644 --- a/orion-visor-ui/src/api/user/user-aggregate.ts +++ b/orion-visor-ui/src/api/user/user-aggregate.ts @@ -1,4 +1,5 @@ import type { MenuQueryResponse } from '@/api/system/menu'; +import type { AxiosResponse } from 'axios'; import axios from 'axios'; /** @@ -35,7 +36,9 @@ export interface UserUpdatePasswordResponse { * 获取用户聚合信息 */ export function getUserAggregateInfo() { - return axios.get('/infra/user-aggregate/user'); + return axios.get>('/infra/user-aggregate/user', { + unwrap: true + }); } /** diff --git a/orion-visor-ui/src/components/app/navbar/index.vue b/orion-visor-ui/src/components/app/navbar/index.vue index 84c446bc..4b022356 100644 --- a/orion-visor-ui/src/components/app/navbar/index.vue +++ b/orion-visor-ui/src/components/app/navbar/index.vue @@ -331,7 +331,7 @@ // 查询未读消息 pullHasUnreadMessage(); // 注册未读消息轮询 - messageIntervalId.value = setInterval(pullHasUnreadMessage, 30000); + messageIntervalId.value = window.setInterval(pullHasUnreadMessage, 30000); }); onUnmounted(() => { diff --git a/orion-visor-ui/src/components/exec/log/panel/index.vue b/orion-visor-ui/src/components/exec/log/panel/index.vue index c6a31209..2375769a 100644 --- a/orion-visor-ui/src/components/exec/log/panel/index.vue +++ b/orion-visor-ui/src/components/exec/log/panel/index.vue @@ -69,7 +69,7 @@ // 等待一秒后先查询一下状态 setTimeout(pullExecStatus, 1000); // 注册状态轮询 - pullIntervalId.value = setInterval(pullExecStatus, 5000); + pullIntervalId.value = window.setInterval(pullExecStatus, 5000); } // 打开日志 nextTick(() => { diff --git a/orion-visor-ui/src/components/exec/log/panel/log-appender.ts b/orion-visor-ui/src/components/exec/log/panel/log-appender.ts index 45597e91..b00c0e79 100644 --- a/orion-visor-ui/src/components/exec/log/panel/log-appender.ts +++ b/orion-visor-ui/src/components/exec/log/panel/log-appender.ts @@ -172,11 +172,11 @@ export default class LogAppender implements ILogAppender { }; this.client.onmessage = this.processMessage.bind(this); // 注册持久化 - this.keepAliveTask = setInterval(() => { + this.keepAliveTask = window.setInterval(() => { if (this.client?.readyState === WebSocket.OPEN) { this.client?.send('p'); } - }, 15000) as unknown as number; + }, 15000); } // 打开日志 diff --git a/orion-visor-ui/src/components/system/message-box/const.ts b/orion-visor-ui/src/components/system/message-box/const.ts index 86a4d398..4f12ef30 100644 --- a/orion-visor-ui/src/components/system/message-box/const.ts +++ b/orion-visor-ui/src/components/system/message-box/const.ts @@ -4,7 +4,7 @@ export const MessageStatus = { READ: 1, }; -export const MESSAGE_CONFIG_KEY = 'messageConfig'; +export const MESSAGE_CONFIG_KEY = 'message-config'; // 查询数量 export const messageLimit = 15; diff --git a/orion-visor-ui/src/components/system/uploader/file-uploader.ts b/orion-visor-ui/src/components/system/uploader/file-uploader.ts index c09b3c1f..aeb45cf9 100644 --- a/orion-visor-ui/src/components/system/uploader/file-uploader.ts +++ b/orion-visor-ui/src/components/system/uploader/file-uploader.ts @@ -2,6 +2,7 @@ import type { FileItem } from '@arco-design/web-vue'; import type { IFileUploader, ResponseMessageBody } from './const'; import { UploadOperatorType, UploadReceiverType } from './const'; import { openFileUploadChannel } from '@/api/system/upload'; +import { closeFileReader } from '@/utils/file'; // 512 KB export const PART_SIZE = 512 * 1024; @@ -81,13 +82,14 @@ export default class FileUploader implements IFileUploader { // 上传下一块数据 private async uploadNextPart() { + let reader = undefined as unknown as FileReader; try { if (this.currentPart < this.totalPart) { // 有下一个分片则上传 const start = this.currentPart++ * PART_SIZE; const end = Math.min(this.currentFile.size, start + PART_SIZE); const chunk = this.currentFile.slice(start, end); - const reader = new FileReader(); + reader = new FileReader(); // 读取数据 const arrayBuffer = await new Promise((resolve, reject) => { reader.onload = () => resolve(reader.result); @@ -107,11 +109,15 @@ export default class FileUploader implements IFileUploader { })); } } catch (e) { - // 读取文件失败 + // 发送读取文件失败 this.client?.send(JSON.stringify({ type: UploadOperatorType.ERROR, fileId: this.currentFileItem.uid, })); + // 释放资源 + if (reader) { + closeFileReader(reader); + } } } diff --git a/orion-visor-ui/src/router/routes/modules/asset-audit.ts b/orion-visor-ui/src/router/routes/modules/asset-audit.ts index 37a0aca4..67ba9dac 100644 --- a/orion-visor-ui/src/router/routes/modules/asset-audit.ts +++ b/orion-visor-ui/src/router/routes/modules/asset-audit.ts @@ -8,19 +8,19 @@ const ASSET_AUDIT: AppRouteRecordRaw[] = [ component: DEFAULT_LAYOUT, children: [ { - name: 'connectLog', - path: '/audit/connect-log', - component: () => import('@/views/asset-audit/connect-log/index.vue'), + name: 'terminalConnectLog', + path: '/audit/terminal-connect-log', + component: () => import('@/views/asset-audit/terminal-connect-log/index.vue'), }, { - name: 'connectSession', - path: '/audit/connect-session', - component: () => import('@/views/asset-audit/connect-session/index.vue'), + name: 'terminalConnectSession', + path: '/audit/terminal-connect-session', + component: () => import('@/views/asset-audit/terminal-connect-session/index.vue'), }, { - name: 'sftpLog', - path: '/audit/sftp-log', - component: () => import('@/views/asset-audit/sftp-log/index.vue'), + name: 'terminalFileLog', + path: '/audit/terminal-file-log', + component: () => import('@/views/asset-audit/terminal-file-log/index.vue'), }, ], }, diff --git a/orion-visor-ui/src/store/modules/terminal/index.ts b/orion-visor-ui/src/store/modules/terminal/index.ts index 3e32a5fb..a7faab8b 100644 --- a/orion-visor-ui/src/store/modules/terminal/index.ts +++ b/orion-visor-ui/src/store/modules/terminal/index.ts @@ -1,13 +1,13 @@ import type { - TerminalInteractSetting, - TerminalPluginsSetting, TerminalPreference, TerminalRdpActionBarSetting, TerminalRdpGraphSetting, - TerminalSessionSetting, + TerminalRdpSessionSetting, TerminalShortcutSetting, TerminalSshActionBarSetting, TerminalSshDisplaySetting, + TerminalSshInteractSetting, + TerminalSshPluginsSetting, TerminalState } from './types'; import type { @@ -34,30 +34,30 @@ import { TerminalSessionTypes, TerminalTabs } from '@/views/terminal/types/const import TerminalTabManager from '@/views/terminal/service/tab/terminal-tab-manager'; import TerminalPanelManager from '@/views/terminal/service/tab/terminal-panel-manager'; import TerminalSessionManager from '@/views/terminal/service/session/terminal-session-manager'; -import SftpTransferManager from '@/views/terminal/service/transfer/sftp-transfer-manager'; +import TerminalTransferManager from '@/views/terminal/service/transfer/terminal-transfer-manager'; // 终端偏好项 export const TerminalPreferenceItem = { // 新建连接类型 NEW_CONNECTION_TYPE: 'newConnectionType', - // 终端主题 - THEME: 'theme', + // ssh 主题 + SSH_THEME: 'sshTheme', // ssh 显示设置 SSH_DISPLAY_SETTING: 'sshDisplaySetting', - // rdp 图形化设置 - RDP_GRAPH_SETTING: 'rdpGraphSetting', // ssh 操作栏设置 SSH_ACTION_BAR_SETTING: 'sshActionBarSetting', + // ssh 右键菜单设置 + SSH_RIGHT_MENU_SETTING: 'sshRightMenuSetting', + // ssh 交互设置 + SSH_INTERACT_SETTING: 'sshInteractSetting', + // ssh 插件设置 + SSH_PLUGINS_SETTING: 'sshPluginsSetting', + // rdp 图形化设置 + RDP_GRAPH_SETTING: 'rdpGraphSetting', // rdp 操作栏设置 RDP_ACTION_BAR_SETTING: 'rdpActionBarSetting', - // 右键菜单设置 - RIGHT_MENU_SETTING: 'rightMenuSetting', - // 交互设置 - INTERACT_SETTING: 'interactSetting', - // 插件设置 - PLUGINS_SETTING: 'pluginsSetting', // 会话设置 - SESSION_SETTING: 'sessionSetting', + RDP_SESSION_SETTING: 'rdpSessionSetting', // 快捷键设置 SHORTCUT_SETTING: 'shortcutSetting', }; @@ -66,17 +66,17 @@ export default defineStore('terminal', { state: (): TerminalState => ({ preference: { newConnectionType: 'group', - theme: { + sshTheme: { schema: {} as TerminalThemeSchema } as TerminalTheme, sshDisplaySetting: {} as TerminalSshDisplaySetting, - rdpGraphSetting: {} as TerminalRdpGraphSetting, sshActionBarSetting: {} as TerminalSshActionBarSetting, + sshRightMenuSetting: [], + sshInteractSetting: {} as TerminalSshInteractSetting, + sshPluginsSetting: {} as TerminalSshPluginsSetting, + rdpGraphSetting: {} as TerminalRdpGraphSetting, + rdpSessionSetting: {} as TerminalRdpSessionSetting, rdpActionBarSetting: {} as TerminalRdpActionBarSetting, - rightMenuSetting: [], - interactSetting: {} as TerminalInteractSetting, - pluginsSetting: {} as TerminalPluginsSetting, - sessionSetting: {} as TerminalSessionSetting, shortcutSetting: { enabled: false, keys: [] @@ -90,7 +90,7 @@ export default defineStore('terminal', { tabManager: new TerminalTabManager(), panelManager: new TerminalPanelManager(), sessionManager: markRaw(new TerminalSessionManager()), - transferManager: new SftpTransferManager(), + transferManager: new TerminalTransferManager(), }), actions: { @@ -100,11 +100,11 @@ export default defineStore('terminal', { // 加载偏好 const { data } = await getPreference('TERMINAL'); // theme 不存在则默认加载第一个 - if (!data.theme?.name) { + if (!data.sshTheme?.name) { const { data: themes } = await getTerminalThemes(); - data.theme = themes[0]; + data.sshTheme = themes[0]; // 更新默认主题偏好 - await this.updateTerminalPreference(TerminalPreferenceItem.THEME, data.theme); + await this.updateTerminalPreference(TerminalPreferenceItem.SSH_THEME, data.sshTheme); } // 移除禁用的快捷键 if (data.shortcutSetting?.enabled) { diff --git a/orion-visor-ui/src/store/modules/terminal/types.ts b/orion-visor-ui/src/store/modules/terminal/types.ts index 7a6e84a6..cb067a25 100644 --- a/orion-visor-ui/src/store/modules/terminal/types.ts +++ b/orion-visor-ui/src/store/modules/terminal/types.ts @@ -1,4 +1,4 @@ -import type { ISftpTransferManager, ITerminalPanelManager, ITerminalSessionManager, ITerminalTabManager, TerminalTheme } from '@/views/terminal/interfaces'; +import type { ITerminalPanelManager, ITerminalSessionManager, ITerminalTabManager, ITerminalTransferManager, TerminalTheme } from '@/views/terminal/interfaces'; import type { AuthorizedHostQueryResponse } from '@/api/asset/asset-authorized-data'; export interface TerminalState { @@ -8,21 +8,21 @@ export interface TerminalState { tabManager: ITerminalTabManager; panelManager: ITerminalPanelManager; sessionManager: ITerminalSessionManager; - transferManager: ISftpTransferManager; + transferManager: ITerminalTransferManager; } // 终端配置 export interface TerminalPreference { newConnectionType: string; - theme: TerminalTheme; + sshTheme: TerminalTheme; sshDisplaySetting: TerminalSshDisplaySetting; - rdpGraphSetting: TerminalRdpGraphSetting; sshActionBarSetting: TerminalSshActionBarSetting; + sshRightMenuSetting: Array, + sshInteractSetting: TerminalSshInteractSetting; + sshPluginsSetting: TerminalSshPluginsSetting; + rdpGraphSetting: TerminalRdpGraphSetting; rdpActionBarSetting: TerminalRdpActionBarSetting; - rightMenuSetting: Array, - interactSetting: TerminalInteractSetting; - pluginsSetting: TerminalPluginsSetting; - sessionSetting: TerminalSessionSetting; + rdpSessionSetting: TerminalRdpSessionSetting; shortcutSetting: TerminalShortcutSetting; } @@ -38,13 +38,44 @@ export interface TerminalSshDisplaySetting { cursorBlink?: boolean; } +// SSH 操作栏设置 +export interface TerminalSshActionBarSetting { + connectStatus?: boolean; + share?: boolean; + + [key: string]: unknown; +} + +// SSH 插件设置 +export interface TerminalSshPluginsSetting { + enableWeblinkPlugin: boolean; + enableWebglPlugin: boolean; + enableUnicodePlugin: boolean; + enableImagePlugin: boolean; +} + +// SSH 交互设置 +export interface TerminalSshInteractSetting { + fastScrollModifier: boolean; + altClickMovesCursor: boolean; + rightClickSelectsWord: boolean; + selectionChangeCopy: boolean; + copyAutoTrim: boolean; + pasteAutoTrim: boolean; + rightClickPaste: boolean; + enableRightClickMenu: boolean; + enableBell: boolean; + wordSeparator: string; + terminalEmulationType: string; + scrollBackLine: number; +} + // RDP 图形化设置 export interface TerminalRdpGraphSetting { displaySize?: string; displayWidth?: number; displayHeight?: number; - enableAudioInput?: boolean; - enableAudioOutput?: boolean; + colorDepth?: number; forceLossless?: boolean; enableWallpaper?: boolean; @@ -56,13 +87,7 @@ export interface TerminalRdpGraphSetting { disableBitmapCaching?: boolean; disableOffscreenCaching?: boolean; disableGlyphCaching?: boolean; -} - -// SSH 操作栏设置 -export interface TerminalSshActionBarSetting { - connectStatus?: boolean; - - [key: string]: unknown; + disableGfx?: boolean; } // RDP 操作栏设置 @@ -79,32 +104,11 @@ export interface TerminalRdpActionBarSetting { [key: string]: unknown; } -// 交互设置 -export interface TerminalInteractSetting { - fastScrollModifier: boolean; - altClickMovesCursor: boolean; - rightClickSelectsWord: boolean; - selectionChangeCopy: boolean; - copyAutoTrim: boolean; - pasteAutoTrim: boolean; - rightClickPaste: boolean; - enableRightClickMenu: boolean; - enableBell: boolean; - wordSeparator: string; -} - -// 插件设置 -export interface TerminalPluginsSetting { - enableWeblinkPlugin: boolean; - enableWebglPlugin: boolean; - enableUnicodePlugin: boolean; - enableImagePlugin: boolean; -} - -// 会话设置 -export interface TerminalSessionSetting { - terminalEmulationType: string; - scrollBackLine: number; +// RDP 会话设置 +export interface TerminalRdpSessionSetting { + enableAudioInput?: boolean; + enableAudioOutput?: boolean; + driveMountMode?: string; } // 终端快捷键设置 diff --git a/orion-visor-ui/src/store/modules/user/index.ts b/orion-visor-ui/src/store/modules/user/index.ts index cc75bcff..8db1ebaf 100644 --- a/orion-visor-ui/src/store/modules/user/index.ts +++ b/orion-visor-ui/src/store/modules/user/index.ts @@ -8,6 +8,41 @@ import { removeRouteListener } from '@/utils/route-listener'; import { getUserAggregateInfo } from '@/api/user/user-aggregate'; import { useAppStore, useCacheStore, useMenuStore, useTabBarStore, useTipsStore } from '@/store'; +const CHECK_APP_VERSION_KEY = 'check-app-version'; + +// 检查版本更新 +const checkForVersionUpdate = (serverVersion: string) => { + try { + if (!serverVersion) { + return; + } + const clientVersion = import.meta.env.VITE_APP_VERSION; + // 版本相同 + if (serverVersion === clientVersion) { + localStorage.removeItem(CHECK_APP_VERSION_KEY); + return; + } + // 版本不同 + const lastCheck = localStorage.getItem(CHECK_APP_VERSION_KEY); + const lastCheckData = lastCheck ? JSON.parse(lastCheck) : null; + // 判断是否是同版本 或 距离上次提醒不超过 24 小时 + if (lastCheckData?.version === serverVersion && Date.now() - (lastCheckData?.time || 0) < 24 * 60 * 60 * 1000) { + return; + } + // 提示用户更新 + if (window.confirm('检测到新版本, 是否刷新页面以获取最新内容?')) { + window.location.reload(); + } + // 更新 localStorage 记录 + localStorage.setItem(CHECK_APP_VERSION_KEY, JSON.stringify({ + version: serverVersion, + time: Date.now(), + })); + } catch (error) { + // ignored + } +}; + export default defineStore('user', { state: (): UserState => ({ id: undefined, @@ -32,7 +67,9 @@ export default defineStore('user', { // 获取用户信息 async getUserInfo() { - const { data } = await getUserAggregateInfo(); + const { data: { data }, headers } = await getUserAggregateInfo(); + // 检查版本更新 + checkForVersionUpdate(headers?.['x-app-version']); // 设置用户信息 this.setUserInfo({ id: data.user.id, diff --git a/orion-visor-ui/src/utils/file.ts b/orion-visor-ui/src/utils/file.ts index ef1994e7..9fb24bc2 100644 --- a/orion-visor-ui/src/utils/file.ts +++ b/orion-visor-ui/src/utils/file.ts @@ -41,6 +41,17 @@ export function readFileText(e: File, encoding = 'UTF-8'): Promise { }); } +// 关闭 fileReader +export function closeFileReader(reader: FileReader) { + // 清理资源 + if (reader.readyState === FileReader.LOADING) { + reader.abort(); + } + reader.onload = null; + reader.onerror = null; + reader.onabort = null; +} + /** * 解析路径类型 */ diff --git a/orion-visor-ui/src/views/asset-audit/sftp-log/types/const.ts b/orion-visor-ui/src/views/asset-audit/sftp-log/types/const.ts deleted file mode 100644 index c911439c..00000000 --- a/orion-visor-ui/src/views/asset-audit/sftp-log/types/const.ts +++ /dev/null @@ -1,20 +0,0 @@ -// 表名称 -export const TableName = 'sftp-log'; - -// sftp 操作类型 -export const SftpOperatorType = { - SFTP_MOVE: 'terminal:sftp-move', - SFTP_CHMOD: 'terminal:sftp-chmod', -}; - -// 最大展示数量 -export const showPathMaxCount = 5; - -// sftp 操作类型 字典项 -export const sftpOperatorTypeKey = 'sftpOperatorType'; - -// sftp 操作结果 字典项 -export const sftpOperatorResultKey = 'operatorLogResult'; - -// 加载的字典值 -export const dictKeys = [sftpOperatorTypeKey, sftpOperatorResultKey]; diff --git a/orion-visor-ui/src/views/asset-audit/connect-log/components/connect-log-clear-modal.vue b/orion-visor-ui/src/views/asset-audit/terminal-connect-log/components/connect-log-clear-modal.vue similarity index 100% rename from orion-visor-ui/src/views/asset-audit/connect-log/components/connect-log-clear-modal.vue rename to orion-visor-ui/src/views/asset-audit/terminal-connect-log/components/connect-log-clear-modal.vue diff --git a/orion-visor-ui/src/views/asset-audit/connect-log/components/connect-log-detail-drawer.vue b/orion-visor-ui/src/views/asset-audit/terminal-connect-log/components/connect-log-detail-drawer.vue similarity index 100% rename from orion-visor-ui/src/views/asset-audit/connect-log/components/connect-log-detail-drawer.vue rename to orion-visor-ui/src/views/asset-audit/terminal-connect-log/components/connect-log-detail-drawer.vue diff --git a/orion-visor-ui/src/views/asset-audit/connect-log/components/connect-log-table.vue b/orion-visor-ui/src/views/asset-audit/terminal-connect-log/components/connect-log-table.vue similarity index 100% rename from orion-visor-ui/src/views/asset-audit/connect-log/components/connect-log-table.vue rename to orion-visor-ui/src/views/asset-audit/terminal-connect-log/components/connect-log-table.vue diff --git a/orion-visor-ui/src/views/asset-audit/connect-log/index.vue b/orion-visor-ui/src/views/asset-audit/terminal-connect-log/index.vue similarity index 97% rename from orion-visor-ui/src/views/asset-audit/connect-log/index.vue rename to orion-visor-ui/src/views/asset-audit/terminal-connect-log/index.vue index c1f222d6..957afee4 100644 --- a/orion-visor-ui/src/views/asset-audit/connect-log/index.vue +++ b/orion-visor-ui/src/views/asset-audit/terminal-connect-log/index.vue @@ -14,7 +14,7 @@ diff --git a/orion-visor-ui/src/views/asset-audit/connect-log/types/const.ts b/orion-visor-ui/src/views/asset-audit/terminal-connect-log/types/const.ts similarity index 100% rename from orion-visor-ui/src/views/asset-audit/connect-log/types/const.ts rename to orion-visor-ui/src/views/asset-audit/terminal-connect-log/types/const.ts diff --git a/orion-visor-ui/src/views/asset-audit/connect-log/types/table.columns.ts b/orion-visor-ui/src/views/asset-audit/terminal-connect-log/types/table.columns.ts similarity index 100% rename from orion-visor-ui/src/views/asset-audit/connect-log/types/table.columns.ts rename to orion-visor-ui/src/views/asset-audit/terminal-connect-log/types/table.columns.ts diff --git a/orion-visor-ui/src/views/asset-audit/connect-session/components/connect-session-table.vue b/orion-visor-ui/src/views/asset-audit/terminal-connect-session/components/connect-session-table.vue similarity index 99% rename from orion-visor-ui/src/views/asset-audit/connect-session/components/connect-session-table.vue rename to orion-visor-ui/src/views/asset-audit/terminal-connect-session/components/connect-session-table.vue index 5dff47dd..5aae12ca 100644 --- a/orion-visor-ui/src/views/asset-audit/connect-session/components/connect-session-table.vue +++ b/orion-visor-ui/src/views/asset-audit/terminal-connect-session/components/connect-session-table.vue @@ -46,7 +46,7 @@
- 主机在线会话 + 终端在线会话
diff --git a/orion-visor-ui/src/views/asset-audit/connect-session/index.vue b/orion-visor-ui/src/views/asset-audit/terminal-connect-session/index.vue similarity index 92% rename from orion-visor-ui/src/views/asset-audit/connect-session/index.vue rename to orion-visor-ui/src/views/asset-audit/terminal-connect-session/index.vue index 7adb4ae6..c9c2ee51 100644 --- a/orion-visor-ui/src/views/asset-audit/connect-session/index.vue +++ b/orion-visor-ui/src/views/asset-audit/terminal-connect-session/index.vue @@ -7,7 +7,7 @@ @@ -18,7 +18,6 @@ import ConnectSessionTable from './components/connect-session-table.vue'; const render = ref(false); - const eventDrawer = ref(); // 加载字典配置 onBeforeMount(async () => { diff --git a/orion-visor-ui/src/views/asset-audit/connect-session/types/const.ts b/orion-visor-ui/src/views/asset-audit/terminal-connect-session/types/const.ts similarity index 100% rename from orion-visor-ui/src/views/asset-audit/connect-session/types/const.ts rename to orion-visor-ui/src/views/asset-audit/terminal-connect-session/types/const.ts diff --git a/orion-visor-ui/src/views/asset-audit/connect-session/types/table.columns.ts b/orion-visor-ui/src/views/asset-audit/terminal-connect-session/types/table.columns.ts similarity index 100% rename from orion-visor-ui/src/views/asset-audit/connect-session/types/table.columns.ts rename to orion-visor-ui/src/views/asset-audit/terminal-connect-session/types/table.columns.ts diff --git a/orion-visor-ui/src/views/asset-audit/sftp-log/components/sftp-log-table.vue b/orion-visor-ui/src/views/asset-audit/terminal-file-log/components/file-log-table.vue similarity index 86% rename from orion-visor-ui/src/views/asset-audit/sftp-log/components/sftp-log-table.vue rename to orion-visor-ui/src/views/asset-audit/terminal-file-log/components/file-log-table.vue index 52511515..554e2174 100644 --- a/orion-visor-ui/src/views/asset-audit/sftp-log/components/sftp-log-table.vue +++ b/orion-visor-ui/src/views/asset-audit/terminal-file-log/components/file-log-table.vue @@ -24,14 +24,14 @@ @@ -50,7 +50,7 @@
- 文件操作日志 + 终端文件日志
@@ -61,7 +61,7 @@ position="br" type="warning" @ok="deleteSelectedRows"> - @@ -110,7 +110,7 @@ @@ -127,11 +127,11 @@ {{ path }} - + 移动到 {{ record.extra?.target }} - + 提权 {{ record.extra?.mod }} {{ permission10toString(record.extra?.mod as number) }} @@ -145,8 +145,8 @@ @@ -169,7 +169,7 @@ position="left" type="warning" @ok="deleteRow(record)"> - @@ -184,15 +184,15 @@ @@ -15,7 +15,7 @@ import { ref, onBeforeMount } from 'vue'; import { useDictStore } from '@/store'; import { dictKeys } from './types/const'; - import SftpLogTable from './components/sftp-log-table.vue'; + import FileLogTable from './components/file-log-table.vue'; const render = ref(false); diff --git a/orion-visor-ui/src/views/asset-audit/terminal-file-log/types/const.ts b/orion-visor-ui/src/views/asset-audit/terminal-file-log/types/const.ts new file mode 100644 index 00000000..904201b2 --- /dev/null +++ b/orion-visor-ui/src/views/asset-audit/terminal-file-log/types/const.ts @@ -0,0 +1,20 @@ +// 表名称 +export const TableName = 'file-log'; + +// 终端文件操作类型 +export const TerminalFileOperatorType = { + SFTP_MOVE: 'terminal:sftp-move', + SFTP_CHMOD: 'terminal:sftp-chmod', +}; + +// 最大展示数量 +export const showPathMaxCount = 5; + +// 终端文件操作类型 字典项 +export const terminalFileOperatorTypeKey = 'terminalFileOperatorType'; + +// 操作结果 字典项 +export const operatorResultKey = 'operatorLogResult'; + +// 加载的字典值 +export const dictKeys = [terminalFileOperatorTypeKey, operatorResultKey]; diff --git a/orion-visor-ui/src/views/asset-audit/sftp-log/types/table.columns.ts b/orion-visor-ui/src/views/asset-audit/terminal-file-log/types/table.columns.ts similarity index 99% rename from orion-visor-ui/src/views/asset-audit/sftp-log/types/table.columns.ts rename to orion-visor-ui/src/views/asset-audit/terminal-file-log/types/table.columns.ts index 146ee855..79822974 100644 --- a/orion-visor-ui/src/views/asset-audit/sftp-log/types/table.columns.ts +++ b/orion-visor-ui/src/views/asset-audit/terminal-file-log/types/table.columns.ts @@ -30,7 +30,7 @@ const columns = [ title: '操作类型', dataIndex: 'type', slotName: 'type', - width: 116, + width: 146, align: 'left', default: true, }, { diff --git a/orion-visor-ui/src/views/asset/host-list/components/host-form-drawer.vue b/orion-visor-ui/src/views/asset/host-list/components/host-form-drawer.vue index 213ec1b9..85dc3ed0 100644 --- a/orion-visor-ui/src/views/asset/host-list/components/host-form-drawer.vue +++ b/orion-visor-ui/src/views/asset/host-list/components/host-form-drawer.vue @@ -133,6 +133,7 @@ // 更新主机信息 const onUpdateHostInfo = (id: number) => { + title.value = '修改主机'; hostId.value = id; hostViewUpdated.value = true; }; diff --git a/orion-visor-ui/src/views/asset/host-list/components/host-form-info.vue b/orion-visor-ui/src/views/asset/host-list/components/host-form-info.vue index 0a45dae8..cc8c76a7 100644 --- a/orion-visor-ui/src/views/asset/host-list/components/host-form-info.vue +++ b/orion-visor-ui/src/views/asset/host-list/components/host-form-info.vue @@ -194,11 +194,13 @@ // 复制 const { data } = await copyHost(formModel.value); Message.success('复制成功'); + formModel.value.id = data; emits('updated', data); } else if (!formModel.value.id) { // 新增 const { data } = await createHost(formModel.value); Message.success('创建成功'); + formModel.value.id = data; emits('updated', data); } else { // 修改 diff --git a/orion-visor-ui/src/views/asset/host-list/components/host-form-rdp.vue b/orion-visor-ui/src/views/asset/host-list/components/host-form-rdp.vue index 942ace48..eadea2fd 100644 --- a/orion-visor-ui/src/views/asset/host-list/components/host-form-rdp.vue +++ b/orion-visor-ui/src/views/asset/host-list/components/host-form-rdp.vue @@ -61,6 +61,7 @@ + @click="router.push({ name: 'terminalConnectLog', query: { action: 'self' } })"> 详情 diff --git a/orion-visor-ui/src/views/dashboard/workplace/components/workplace-statistics.vue b/orion-visor-ui/src/views/dashboard/workplace/components/workplace-statistics.vue index bc3b0123..b449436b 100644 --- a/orion-visor-ui/src/views/dashboard/workplace/components/workplace-statistics.vue +++ b/orion-visor-ui/src/views/dashboard/workplace/components/workplace-statistics.vue @@ -99,7 +99,7 @@ background: isDark.value ? '#354276' : '#E8F3FF', iconColor: isDark.value ? '#4A7FF7' : '#165DFF', }, - go: () => router.push({ name: 'connectLog', query: { action: 'self' } }) + go: () => router.push({ name: 'terminalConnectLog', query: { action: 'self' } }) }, { title: '今日批量执行次数', value: props.data.exec?.todayExecCommandCount || 0, diff --git a/orion-visor-ui/src/views/exec/batch-upload/components/upload-panel.vue b/orion-visor-ui/src/views/exec/batch-upload/components/upload-panel.vue index ba334b8e..b3b9613b 100644 --- a/orion-visor-ui/src/views/exec/batch-upload/components/upload-panel.vue +++ b/orion-visor-ui/src/views/exec/batch-upload/components/upload-panel.vue @@ -278,7 +278,7 @@ // 设置轮询状态 onMounted(() => { - pullIntervalId.value = setInterval(pullTaskStatus, 5000); + pullIntervalId.value = window.setInterval(pullTaskStatus, 5000); }); // 卸载状态查询 diff --git a/orion-visor-ui/src/views/exec/exec-command-log/components/exec-command-log-table.vue b/orion-visor-ui/src/views/exec/exec-command-log/components/exec-command-log-table.vue index b5beea43..d1bd6bc7 100644 --- a/orion-visor-ui/src/views/exec/exec-command-log/components/exec-command-log-table.vue +++ b/orion-visor-ui/src/views/exec/exec-command-log/components/exec-command-log-table.vue @@ -429,7 +429,7 @@ // 加载数据 fetchTableData(); // 注册状态轮询 - pullIntervalId.value = setInterval(pullExecStatus, 10000); + pullIntervalId.value = window.setInterval(pullExecStatus, 10000); }); onUnmounted(() => { diff --git a/orion-visor-ui/src/views/exec/exec-job-log/components/exec-job-log-table.vue b/orion-visor-ui/src/views/exec/exec-job-log/components/exec-job-log-table.vue index 40bc2bc0..460f37f7 100644 --- a/orion-visor-ui/src/views/exec/exec-job-log/components/exec-job-log-table.vue +++ b/orion-visor-ui/src/views/exec/exec-job-log/components/exec-job-log-table.vue @@ -417,7 +417,7 @@ // 加载数据 fetchTableData(); // 注册状态轮询 - pullIntervalId.value = setInterval(pullJobStatus, 10000); + pullIntervalId.value = window.setInterval(pullJobStatus, 10000); }); onUnmounted(() => { diff --git a/orion-visor-ui/src/views/exec/upload-task/components/upload-task-table.vue b/orion-visor-ui/src/views/exec/upload-task/components/upload-task-table.vue index a9129fde..7a154336 100644 --- a/orion-visor-ui/src/views/exec/upload-task/components/upload-task-table.vue +++ b/orion-visor-ui/src/views/exec/upload-task/components/upload-task-table.vue @@ -324,7 +324,7 @@ // 加载数据 fetchTableData(); // 注册状态轮询 - pullIntervalId.value = setInterval(pullTaskStatus, 10000); + pullIntervalId.value = window.setInterval(pullTaskStatus, 10000); }); onUnmounted(() => { diff --git a/orion-visor-ui/src/views/system/setting/components/about-setting.vue b/orion-visor-ui/src/views/system/setting/components/about-setting.vue index b115b5a2..9bb99c58 100644 --- a/orion-visor-ui/src/views/system/setting/components/about-setting.vue +++ b/orion-visor-ui/src/views/system/setting/components/about-setting.vue @@ -6,12 +6,6 @@ :align="{ label: 'right', value: 'left' }" :label-style="{ paddingTop: '2px', paddingLeft: '32px', verticalAlign: 'top' }" :column="1"> - - - - {{ app.uuid }} - - @@ -66,7 +60,6 @@ const app = ref({ version: '', - uuid: '', }); const repo = ref({ diff --git a/orion-visor-ui/src/views/terminal/components/command-snippet/command-snippet-form-drawer.vue b/orion-visor-ui/src/views/terminal/components/command-snippet/command-snippet-form-drawer.vue index 21065c39..7aa7bc08 100644 --- a/orion-visor-ui/src/views/terminal/components/command-snippet/command-snippet-form-drawer.vue +++ b/orion-visor-ui/src/views/terminal/components/command-snippet/command-snippet-form-drawer.vue @@ -13,7 +13,7 @@ ref="formRef" label-align="right" :auto-label-width="true" - :rules="formRules"> + :rules="commandSnippetFormRules"> ; diff --git a/orion-visor-ui/src/views/terminal/components/path-bookmark/path-bookmark-form-drawer.vue b/orion-visor-ui/src/views/terminal/components/path-bookmark/path-bookmark-form-drawer.vue index 9da50091..2110a4f3 100644 --- a/orion-visor-ui/src/views/terminal/components/path-bookmark/path-bookmark-form-drawer.vue +++ b/orion-visor-ui/src/views/terminal/components/path-bookmark/path-bookmark-form-drawer.vue @@ -13,7 +13,7 @@ ref="formRef" label-align="right" :auto-label-width="true" - :rules="formRules"> + :rules="bookmarkFormRules"> ; diff --git a/orion-visor-ui/src/views/terminal/components/setting/block-setting-item.vue b/orion-visor-ui/src/views/terminal/components/setting/block-setting-item.vue index 1b1a5e11..3dda1579 100644 --- a/orion-visor-ui/src/views/terminal/components/setting/block-setting-item.vue +++ b/orion-visor-ui/src/views/terminal/components/setting/block-setting-item.vue @@ -13,12 +13,12 @@
- - @@ -24,8 +24,8 @@ diff --git a/orion-visor-ui/src/views/terminal/components/setting/display/terminal-ssh-display-block.vue b/orion-visor-ui/src/views/terminal/components/setting/display/terminal-ssh-display-block.vue index 4d9a7bb6..6231e5d0 100644 --- a/orion-visor-ui/src/views/terminal/components/setting/display/terminal-ssh-display-block.vue +++ b/orion-visor-ui/src/views/terminal/components/setting/display/terminal-ssh-display-block.vue @@ -3,7 +3,7 @@

- 显示偏好 + SSH 显示偏好

@@ -99,7 +99,7 @@ 预览效果
-
@@ -127,7 +127,7 @@ const { toOptions, toRadioOptions } = useDictStore(); const { preference, updateTerminalPreference, sessionManager } = useTerminalStore(); - const background = preference.theme.schema.background; + const background = preference.sshTheme.schema.background; const previewTerminal = ref(); const formModel = ref({}); diff --git a/orion-visor-ui/src/views/terminal/components/setting/display/terminal-right-menu-block.vue b/orion-visor-ui/src/views/terminal/components/setting/display/terminal-ssh-right-menu-block.vue similarity index 95% rename from orion-visor-ui/src/views/terminal/components/setting/display/terminal-right-menu-block.vue rename to orion-visor-ui/src/views/terminal/components/setting/display/terminal-ssh-right-menu-block.vue index 2d5bb1b1..02ef7f63 100644 --- a/orion-visor-ui/src/views/terminal/components/setting/display/terminal-right-menu-block.vue +++ b/orion-visor-ui/src/views/terminal/components/setting/display/terminal-ssh-right-menu-block.vue @@ -3,7 +3,7 @@

- 右键菜单设置 + SSH 右键菜单设置

@@ -80,7 +80,7 @@ @@ -95,12 +95,12 @@ const { preference, updateTerminalPreference } = useTerminalStore(); const popupContainer = ref(); - const rightActionItems = ref>([...preference.rightMenuSetting]); + const rightActionItems = ref>([...preference.sshRightMenuSetting]); // // 监听同步 watch(rightActionItems, (v) => { // 同步 - updateTerminalPreference(TerminalPreferenceItem.RIGHT_MENU_SETTING, JSON.stringify(v), true); + updateTerminalPreference(TerminalPreferenceItem.SSH_RIGHT_MENU_SETTING, JSON.stringify(v), true); }, { deep: true }); // 实际操作项 diff --git a/orion-visor-ui/src/views/terminal/components/setting/extra/rdp-extra-form.vue b/orion-visor-ui/src/views/terminal/components/setting/extra/rdp-extra-form.vue index 2a4830e8..e4570293 100644 --- a/orion-visor-ui/src/views/terminal/components/setting/extra/rdp-extra-form.vue +++ b/orion-visor-ui/src/views/terminal/components/setting/extra/rdp-extra-form.vue @@ -21,10 +21,19 @@
+ + + + diff --git a/orion-visor-ui/src/views/terminal/components/setting/general/terminal-general-setting.vue b/orion-visor-ui/src/views/terminal/components/setting/general/terminal-general-setting.vue index 2aba404b..113659c3 100644 --- a/orion-visor-ui/src/views/terminal/components/setting/general/terminal-general-setting.vue +++ b/orion-visor-ui/src/views/terminal/components/setting/general/terminal-general-setting.vue @@ -3,14 +3,14 @@

终端设置

- - - - - - + + + + + +
@@ -22,10 +22,10 @@ diff --git a/orion-visor-ui/src/views/terminal/components/setting/general/terminal-rdp-graph-block.vue b/orion-visor-ui/src/views/terminal/components/setting/general/terminal-rdp-graph-block.vue index 278c62ef..1d588c2f 100644 --- a/orion-visor-ui/src/views/terminal/components/setting/general/terminal-rdp-graph-block.vue +++ b/orion-visor-ui/src/views/terminal/components/setting/general/terminal-rdp-graph-block.vue @@ -3,7 +3,7 @@

- RDP 设置 + RDP 图形化设置

@@ -28,61 +28,61 @@ - - - - - - + + - - + + - - + + - - + + - - + + + + + + diff --git a/orion-visor-ui/src/views/terminal/components/setting/general/terminal-rdp-session-block.vue b/orion-visor-ui/src/views/terminal/components/setting/general/terminal-rdp-session-block.vue new file mode 100644 index 00000000..763e604a --- /dev/null +++ b/orion-visor-ui/src/views/terminal/components/setting/general/terminal-rdp-session-block.vue @@ -0,0 +1,66 @@ + + + + + + + diff --git a/orion-visor-ui/src/views/terminal/components/setting/general/terminal-session-block.vue b/orion-visor-ui/src/views/terminal/components/setting/general/terminal-session-block.vue deleted file mode 100644 index a69b6c97..00000000 --- a/orion-visor-ui/src/views/terminal/components/setting/general/terminal-session-block.vue +++ /dev/null @@ -1,70 +0,0 @@ - - - - - - - diff --git a/orion-visor-ui/src/views/terminal/components/setting/general/terminal-interact-block.vue b/orion-visor-ui/src/views/terminal/components/setting/general/terminal-ssh-interact-block.vue similarity index 74% rename from orion-visor-ui/src/views/terminal/components/setting/general/terminal-interact-block.vue rename to orion-visor-ui/src/views/terminal/components/setting/general/terminal-ssh-interact-block.vue index 9973f300..9d2bdfbe 100644 --- a/orion-visor-ui/src/views/terminal/components/setting/general/terminal-interact-block.vue +++ b/orion-visor-ui/src/views/terminal/components/setting/general/terminal-ssh-interact-block.vue @@ -3,7 +3,7 @@

- 交互设置 + SSH 交互设置

@@ -56,9 +56,6 @@ - @@ -75,27 +72,49 @@ allow-clear /> + + + + + + + + + + diff --git a/orion-visor-ui/src/views/terminal/components/setting/general/terminal-plugins-block.vue b/orion-visor-ui/src/views/terminal/components/setting/general/terminal-ssh-plugins-block.vue similarity index 86% rename from orion-visor-ui/src/views/terminal/components/setting/general/terminal-plugins-block.vue rename to orion-visor-ui/src/views/terminal/components/setting/general/terminal-ssh-plugins-block.vue index 09e8bf58..520a18f5 100644 --- a/orion-visor-ui/src/views/terminal/components/setting/general/terminal-plugins-block.vue +++ b/orion-visor-ui/src/views/terminal/components/setting/general/terminal-ssh-plugins-block.vue @@ -3,7 +3,7 @@

- 插件设置 + SSH 插件设置

@@ -34,12 +34,12 @@ diff --git a/orion-visor-ui/src/views/terminal/components/setting/theme/terminal-theme-block.vue b/orion-visor-ui/src/views/terminal/components/setting/theme/terminal-ssh-theme-block.vue similarity index 94% rename from orion-visor-ui/src/views/terminal/components/setting/theme/terminal-theme-block.vue rename to orion-visor-ui/src/views/terminal/components/setting/theme/terminal-ssh-theme-block.vue index 317c64b9..4555d603 100644 --- a/orion-visor-ui/src/views/terminal/components/setting/theme/terminal-theme-block.vue +++ b/orion-visor-ui/src/views/terminal/components/setting/theme/terminal-ssh-theme-block.vue @@ -3,7 +3,7 @@

- 主题设置 + SSH 主题设置

@@ -49,7 +49,7 @@ @@ -86,14 +86,14 @@ }); // 同步 currentThemeName.value = theme.name; - await updateTerminalPreference(TerminalPreferenceItem.THEME, theme, true); + await updateTerminalPreference(TerminalPreferenceItem.SSH_THEME, theme, true); }; // 加载用户主题 onMounted(async () => { try { - const { data } = await getPreference>('TERMINAL', [TerminalPreferenceItem.THEME]); - currentThemeName.value = data[TerminalPreferenceItem.THEME]?.name; + const { data } = await getPreference>('TERMINAL', [TerminalPreferenceItem.SSH_THEME]); + currentThemeName.value = data[TerminalPreferenceItem.SSH_THEME]?.name; } catch (e) { } }); diff --git a/orion-visor-ui/src/views/terminal/components/setting/theme/terminal-theme-setting.vue b/orion-visor-ui/src/views/terminal/components/setting/theme/terminal-theme-setting.vue index ec244ab5..278a7e11 100644 --- a/orion-visor-ui/src/views/terminal/components/setting/theme/terminal-theme-setting.vue +++ b/orion-visor-ui/src/views/terminal/components/setting/theme/terminal-theme-setting.vue @@ -3,8 +3,8 @@

主题设置

- - + +
@@ -16,7 +16,7 @@ diff --git a/orion-visor-ui/src/views/terminal/components/transfer/transfer-drawer.vue b/orion-visor-ui/src/views/terminal/components/transfer/transfer-drawer.vue index fed074de..933547cb 100644 --- a/orion-visor-ui/src/views/terminal/components/transfer/transfer-drawer.vue +++ b/orion-visor-ui/src/views/terminal/components/transfer/transfer-drawer.vue @@ -37,7 +37,7 @@ - {{ transferManager.transferList.filter(s => s.status === option.value).length }} + {{ transferTasks.filter(s => s.state.status === option.value).length }} @@ -48,15 +48,15 @@ max-height="100%" :hoverable="true" :bordered="false" - :data="transferManager.transferList"> + :data="transferTasks"> @@ -70,13 +70,13 @@ diff --git a/orion-visor-ui/src/views/terminal/components/transfer/transfer-item.vue b/orion-visor-ui/src/views/terminal/components/transfer/transfer-task.vue similarity index 65% rename from orion-visor-ui/src/views/terminal/components/transfer/transfer-item.vue rename to orion-visor-ui/src/views/terminal/components/transfer/transfer-task.vue index 691d92e5..43e39d1b 100644 --- a/orion-visor-ui/src/views/terminal/components/transfer/transfer-item.vue +++ b/orion-visor-ui/src/views/terminal/components/transfer/transfer-task.vue @@ -4,46 +4,46 @@
- - + +
- {{ item.name }} + :title="task.fileItem.name" + @click="copy(task.fileItem.name)"> + {{ task.fileItem.name }} - {{ getFileSize(item.currentSize) }} - / + {{ getFileSize(task.state.currentSize) }} + / - {{ getFileSize(item.totalSize) }} + {{ getFileSize(task.state.totalSize) }} - - {{ item.progress }}% + + {{ task.state.progress }}% - {{ item.parentPath }} + :title="task.fileItem.parentPath" + @click="copy(task.fileItem.parentPath)"> + {{ task.fileItem.parentPath }} - + :content="task.state.errorMessage"> - {{ item.errorMessage }} + {{ task.state.errorMessage }}
@@ -52,18 +52,20 @@
- + + + + :status="getDictValue(transferStatusKey, task.state.status, 'status')" + :percent="task.state.currentSize / task.state.totalSize" />
- +
@@ -74,21 +76,21 @@ diff --git a/orion-visor-ui/src/views/terminal/components/view/rdp/rdp-action-bar.vue b/orion-visor-ui/src/views/terminal/components/view/rdp/rdp-action-bar.vue index d3dd25bc..631116ac 100644 --- a/orion-visor-ui/src/views/terminal/components/view/rdp/rdp-action-bar.vue +++ b/orion-visor-ui/src/views/terminal/components/view/rdp/rdp-action-bar.vue @@ -1,5 +1,5 @@