@@ -15,6 +15,8 @@ MYSQL_ROOT_PASSWORD=Data@123456
|
||||
|
||||
REDIS_HOST=redis
|
||||
REDIS_PASSWORD=Data@123456
|
||||
REDIS_DATABASE=0
|
||||
REDIS_DATA_VERSION=1
|
||||
|
||||
GUACD_HOST=guacd
|
||||
GUACD_PORT=4822
|
||||
|
||||
190
.github/workflows/docker-publish.yml
vendored
190
.github/workflows/docker-publish.yml
vendored
@@ -3,127 +3,131 @@ name: Docker Publish
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*' # Trigger on version tags like v1.0.0
|
||||
workflow_dispatch: # Allow manual trigger
|
||||
- 'v*'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build-and-push:
|
||||
build-project:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read # To read repository content
|
||||
packages: write # To push packages to GitHub Container Registry
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
env:
|
||||
DOCKERHUB_USERNAME: ${{ vars.DOCKERHUB_ORGNAME }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
- name: 🌱 Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up QEMU
|
||||
- name: ⚙️ Set up JDK 8
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: '8'
|
||||
distribution: 'temurin'
|
||||
cache: 'maven'
|
||||
|
||||
- name: ⚙️ Set up Node.js 18
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '18'
|
||||
|
||||
- name: 🔧 Install pnpm
|
||||
run: npm i -g pnpm
|
||||
|
||||
- name: 📦 Build Java
|
||||
run: mvn -U clean install -DskipTests
|
||||
|
||||
- name: 📦️ Build UI
|
||||
working-directory: ./orion-visor-ui
|
||||
run: |
|
||||
pnpm install
|
||||
pnpm build
|
||||
|
||||
- name: 📁 Prepare build context
|
||||
run: |
|
||||
cp -r ./sql ./docker/mysql/sql
|
||||
cp -r ./orion-visor-ui/dist ./docker/ui/dist
|
||||
cp ./orion-visor-launch/target/orion-visor-launch.jar ./docker/service/orion-visor-launch.jar
|
||||
|
||||
- name: 📤 Upload build context
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: docker-context
|
||||
path: docker
|
||||
|
||||
build-and-push:
|
||||
needs: build-project
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
service: [ adminer, guacd, mysql, redis, service, ui ]
|
||||
|
||||
env:
|
||||
GITHUB_REGISTRY: ghcr.io
|
||||
ALIYUN_REGISTRY: registry.cn-hangzhou.aliyuncs.com
|
||||
ALIYUN_NAMESPACE: ${{ vars.ALIYUN_NAMESPACE }}
|
||||
DOCKERHUB_NAMESPACE: ${{ vars.DOCKERHUB_NAMESPACE }}
|
||||
|
||||
steps:
|
||||
- name: 📥 Download build context
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: docker-context
|
||||
path: docker
|
||||
|
||||
- name: ⚙️ Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
- name: 🔧 Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to Docker Hub
|
||||
- name: 🐳 Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
- name: 🐳 Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
registry: ${{ env.GITHUB_REGISTRY }}
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract Docker metadata
|
||||
id: meta # Giving an ID to this step to reference its outputs later
|
||||
- name: 🐳 Login to Aliyun Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.ALIYUN_REGISTRY }}
|
||||
username: ${{ secrets.ALIYUN_USERNAME }}
|
||||
password: ${{ secrets.ALIYUN_TOKEN }}
|
||||
|
||||
- name: 📦 Extract Docker metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: | # Define base image names for metadata generation
|
||||
orion-visor-adminer
|
||||
orion-visor-guacd
|
||||
orion-visor-mysql
|
||||
orion-visor-redis
|
||||
orion-visor-service
|
||||
orion-visor-ui
|
||||
tags: | # Define how tags are generated
|
||||
type=semver,pattern={{version}} # Main strategy: git tag v1.2.3 will produce tag 1.2.3
|
||||
type=semver,pattern={{major}}.{{minor}} # e.g., v1.2.3 -> 1.2
|
||||
type=semver,pattern={{major}} # e.g., v1.2.3 -> 1
|
||||
images: |
|
||||
${{ env.DOCKERHUB_NAMESPACE }}/orion-visor-${{ matrix.service }}
|
||||
${{ env.GITHUB_REGISTRY }}/${{ github.repository_owner }}/orion-visor-${{ matrix.service }}
|
||||
${{ env.ALIYUN_REGISTRY }}/${{ env.ALIYUN_NAMESPACE }}/orion-visor-${{ matrix.service }}
|
||||
tags: |
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=semver,pattern={{major}}
|
||||
|
||||
# --- Build and push generic images ---
|
||||
|
||||
- name: Build and push orion-visor-adminer
|
||||
- name: 🛠️ Build and push Docker image for orion-visor-${{ matrix.service }}
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ./docker/adminer/Dockerfile
|
||||
context: ./docker
|
||||
file: ./docker/${{ matrix.service }}/Dockerfile
|
||||
push: true
|
||||
tags: |
|
||||
${{ env.DOCKERHUB_USERNAME }}/orion-visor-adminer:${{ steps.meta.outputs.version }}
|
||||
ghcr.io/${{ github.repository_owner }}/orion-visor-adminer:${{ steps.meta.outputs.version }}
|
||||
${{ env.DOCKERHUB_NAMESPACE }}/orion-visor-${{ matrix.service }}:${{ steps.meta.outputs.version }}
|
||||
${{ env.DOCKERHUB_NAMESPACE }}/orion-visor-${{ matrix.service }}:latest
|
||||
${{ env.GITHUB_REGISTRY }}/${{ github.repository_owner }}/orion-visor-${{ matrix.service }}:${{ steps.meta.outputs.version }}
|
||||
${{ env.GITHUB_REGISTRY }}/${{ github.repository_owner }}/orion-visor-${{ matrix.service }}:latest
|
||||
${{ env.ALIYUN_REGISTRY }}/${{ env.ALIYUN_NAMESPACE }}/orion-visor-${{ matrix.service }}:${{ steps.meta.outputs.version }}
|
||||
${{ env.ALIYUN_REGISTRY }}/${{ env.ALIYUN_NAMESPACE }}/orion-visor-${{ matrix.service }}:latest
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
|
||||
- name: Build and push orion-visor-guacd
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ./docker/guacd/Dockerfile
|
||||
push: true
|
||||
tags: |
|
||||
${{ env.DOCKERHUB_USERNAME }}/orion-visor-guacd:${{ steps.meta.outputs.version }}
|
||||
ghcr.io/${{ github.repository_owner }}/orion-visor-guacd:${{ steps.meta.outputs.version }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
|
||||
- name: Build and push orion-visor-mysql
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ./docker/mysql/Dockerfile
|
||||
push: true
|
||||
tags: |
|
||||
${{ env.DOCKERHUB_USERNAME }}/orion-visor-mysql:${{ steps.meta.outputs.version }}
|
||||
ghcr.io/${{ github.repository_owner }}/orion-visor-mysql:${{ steps.meta.outputs.version }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
|
||||
- name: Build and push orion-visor-redis
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ./docker/redis/Dockerfile
|
||||
push: true
|
||||
tags: |
|
||||
${{ env.DOCKERHUB_USERNAME }}/orion-visor-redis:${{ steps.meta.outputs.version }}
|
||||
ghcr.io/${{ github.repository_owner }}/orion-visor-redis:${{ steps.meta.outputs.version }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
|
||||
- name: Build and push orion-visor-service
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ./docker/service/Dockerfile
|
||||
push: true
|
||||
tags: |
|
||||
${{ env.DOCKERHUB_USERNAME }}/orion-visor-service:${{ steps.meta.outputs.version }}
|
||||
ghcr.io/${{ github.repository_owner }}/orion-visor-service:${{ steps.meta.outputs.version }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
|
||||
- name: Build and push orion-visor-ui
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ./docker/ui/Dockerfile
|
||||
push: true
|
||||
tags: |
|
||||
${{ env.DOCKERHUB_USERNAME }}/orion-visor-ui:${{ steps.meta.outputs.version }}
|
||||
ghcr.io/${{ github.repository_owner }}/orion-visor-ui:${{ steps.meta.outputs.version }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
platforms: linux/amd64,linux/arm64 # Uncomment for multi-platform builds
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
**`orion-visor`** 提供一站式自动化运维解决方案。
|
||||
|
||||
* **资产管理**:支持对资产进行分组,实现对主机、密钥和身份的统一管理和授权。
|
||||
* **在线终端**:提供在线终端 SSH/RDP 等多种协议,支持快捷命令、自定义快捷键和主题风格。
|
||||
* **在线终端**:提供在线终端 SSH/RDP/VNC 等多种协议,支持快捷命令、自定义快捷键和主题风格。
|
||||
* **文件管理**:支持远程主机 SFTP 大文件的批量上传、下载和在线编辑等操作。
|
||||
* **批量操作**:支持批量执行主机命令、多主机文件分发等功能。
|
||||
* **计划任务**:支持配置 cron 表达式,定时执行主机命令。
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
#/bin/bash
|
||||
set -e
|
||||
|
||||
# ./build_docker.sh --push 这样使用会编译完成后自动推送镜像到阿里云仓库
|
||||
version=2.4.1
|
||||
push_images=false
|
||||
|
||||
# 解析参数
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--push)
|
||||
push_images=true
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
echo "未知参数: $1"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
docker build -f ./docker/ui/Dockerfile -t orion-visor-ui:${version} -t registry.cn-hangzhou.aliyuncs.com/orionsec/orion-visor-ui:${version} . && \
|
||||
docker build -f ./docker/service/Dockerfile -t orion-visor-service:${version} -t registry.cn-hangzhou.aliyuncs.com/orionsec/orion-visor-service:${version} . && \
|
||||
docker build -f ./docker/mysql/Dockerfile -t orion-visor-mysql:${version} -t registry.cn-hangzhou.aliyuncs.com/orionsec/orion-visor-mysql:${version} . && \
|
||||
docker build -f ./docker/redis/Dockerfile -t orion-visor-redis:${version} -t registry.cn-hangzhou.aliyuncs.com/orionsec/orion-visor-redis:${version} . && \
|
||||
docker build -f ./docker/adminer/Dockerfile -t orion-visor-adminer:${version} -t registry.cn-hangzhou.aliyuncs.com/orionsec/orion-visor-adminer:${version} . && \
|
||||
docker build -f ./docker/guacd/Dockerfile -t orion-visor-guacd:${version} -t registry.cn-hangzhou.aliyuncs.com/orionsec/orion-visor-guacd:${version} .
|
||||
|
||||
|
||||
# 如果需要推送镜像
|
||||
if [ "$push_images" = true ]; then
|
||||
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}
|
||||
docker push registry.cn-hangzhou.aliyuncs.com/orionsec/orion-visor-guacd:${version}
|
||||
docker push registry.cn-hangzhou.aliyuncs.com/orionsec/orion-visor-service:${version}
|
||||
docker push registry.cn-hangzhou.aliyuncs.com/orionsec/orion-visor-ui:${version}
|
||||
fi
|
||||
@@ -1,6 +1,12 @@
|
||||
version: '3.3'
|
||||
|
||||
# latest = 2.4.1
|
||||
# latest = 2.4.2
|
||||
|
||||
# 支持以下源
|
||||
# lijiahangmax/*
|
||||
# ghcr.io/dromara/*
|
||||
# registry.cn-hangzhou.aliyuncs.com/orionsec/*
|
||||
|
||||
services:
|
||||
ui:
|
||||
image: registry.cn-hangzhou.aliyuncs.com/orionsec/orion-visor-ui:latest
|
||||
@@ -27,6 +33,8 @@ services:
|
||||
MYSQL_PASSWORD: ${MYSQL_PASSWORD:-Data@123456}
|
||||
REDIS_HOST: ${REDIS_HOST:-redis}
|
||||
REDIS_PASSWORD: ${REDIS_PASSWORD:-Data@123456}
|
||||
REDIS_DATABASE: ${REDIS_DATABASE:-0}
|
||||
REDIS_DATA_VERSION: ${REDIS_DATA_VERSION:-1}
|
||||
GUACD_HOST: ${GUACD_HOST:-guacd}
|
||||
GUACD_PORT: ${GUACD_PORT:-4822}
|
||||
GUACD_DRIVE_PATH: ${GUACD_DRIVE_PATH:-/drive}
|
||||
|
||||
@@ -1,16 +1,51 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 停止并移除现有容器
|
||||
docker compose down --remove-orphans
|
||||
# 初始化标志变量
|
||||
PULL_IMAGES=false
|
||||
DEMO_MODE=false
|
||||
|
||||
if [ "$1" == "demo" ]; then
|
||||
# 设置 DEMO_MODE 环境变量为 true
|
||||
# 解析命令行参数
|
||||
for arg in "$@"
|
||||
do
|
||||
case $arg in
|
||||
--pull)
|
||||
PULL_IMAGES=true
|
||||
shift
|
||||
;;
|
||||
--demo)
|
||||
DEMO_MODE=true
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
echo "Unknown argument: $arg"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# 停止并移除现有容器
|
||||
echo "Stopping all services..."
|
||||
docker compose down --remove-orphans
|
||||
echo "Stopped all services..."
|
||||
|
||||
# 拉取镜像
|
||||
if [ "$PULL_IMAGES" = true ]; then
|
||||
echo "Pulling latest images..."
|
||||
docker compose pull
|
||||
echo "Pulled latest images..."
|
||||
fi
|
||||
|
||||
if [ "$DEMO_MODE" = true ]; then
|
||||
# 启用 demo 模式
|
||||
export DEMO_MODE=true
|
||||
echo "Starting services for demo mode..."
|
||||
|
||||
# 启动指定的服务
|
||||
docker compose up -d --remove-orphans mysql redis ui service adminer
|
||||
docker compose up -d --remove-orphans mysql redis ui service guacd adminer
|
||||
echo "Started services for demo mode..."
|
||||
else
|
||||
# 启动所有服务
|
||||
echo "Starting all services..."
|
||||
# 正常启动所有服务
|
||||
docker compose up -d --remove-orphans
|
||||
echo "Started all services..."
|
||||
fi
|
||||
|
||||
16
docker/builder/Dockerfile.service
Normal file
16
docker/builder/Dockerfile.service
Normal file
@@ -0,0 +1,16 @@
|
||||
FROM maven:3.9.10-eclipse-temurin-8-alpine AS builder
|
||||
|
||||
# 设置阿里云镜像加速
|
||||
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories
|
||||
|
||||
# 拷贝 settings.xml
|
||||
COPY ./docker/builder/maven-settings.xml /root/.m2/settings.xml
|
||||
|
||||
WORKDIR /build
|
||||
|
||||
COPY . .
|
||||
|
||||
# 复制 POM 文件先进行依赖下载 (利用 Docker 缓存)
|
||||
RUN mvn dependency:go-offline --settings=/root/.m2/settings.xml
|
||||
# 构建
|
||||
RUN mvn clean package -DskipTests --settings=/root/.m2/settings.xml
|
||||
25
docker/builder/Dockerfile.ui
Normal file
25
docker/builder/Dockerfile.ui
Normal file
@@ -0,0 +1,25 @@
|
||||
FROM node:18-alpine AS builder
|
||||
|
||||
# 设置阿里云镜像加速
|
||||
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories
|
||||
|
||||
# 安装 pnpm
|
||||
RUN corepack enable && corepack prepare pnpm@latest --activate
|
||||
|
||||
WORKDIR /build
|
||||
|
||||
# 设置 pnpm 使用指定的 registry
|
||||
ARG REGISTRY_URL=https://registry.npmmirror.com
|
||||
RUN pnpm config set registry $REGISTRY_URL
|
||||
|
||||
# 复制项目文件
|
||||
COPY ./orion-visor-ui/package.json ./orion-visor-ui/pnpm-lock.yaml* ./
|
||||
|
||||
# 安装依赖 (利用 Docker 缓存层)
|
||||
RUN pnpm install --frozen-lockfile
|
||||
|
||||
# 复制源代码
|
||||
COPY ./orion-visor-ui/ .
|
||||
|
||||
# 构建项目
|
||||
RUN pnpm build
|
||||
54
docker/builder/maven-settings.xml
Normal file
54
docker/builder/maven-settings.xml
Normal file
@@ -0,0 +1,54 @@
|
||||
<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 http://maven.apache.org/xsd/settings-1.0.0.xsd">
|
||||
|
||||
<profiles>
|
||||
<profile>
|
||||
<id>repos</id>
|
||||
<repositories>
|
||||
<!-- 阿里云 Maven 公共仓库 -->
|
||||
<repository>
|
||||
<id>aliyun</id>
|
||||
<name>Aliyun Repository</name>
|
||||
<url>https://maven.aliyun.com/repository/public</url>
|
||||
<releases>
|
||||
<enabled>true</enabled>
|
||||
</releases>
|
||||
<snapshots>
|
||||
<enabled>true</enabled>
|
||||
</snapshots>
|
||||
</repository>
|
||||
|
||||
<!-- Maven 中央仓库 -->
|
||||
<repository>
|
||||
<id>central</id>
|
||||
<name>Maven Central Repository</name>
|
||||
<url>https://repo.maven.apache.org/maven2</url>
|
||||
<releases>
|
||||
<enabled>true</enabled>
|
||||
</releases>
|
||||
<snapshots>
|
||||
<enabled>false</enabled>
|
||||
</snapshots>
|
||||
</repository>
|
||||
</repositories>
|
||||
|
||||
<pluginRepositories>
|
||||
<pluginRepository>
|
||||
<id>aliyun-plugin</id>
|
||||
<url>https://maven.aliyun.com/repository/public</url>
|
||||
</pluginRepository>
|
||||
<pluginRepository>
|
||||
<id>central-plugin</id>
|
||||
<url>https://repo.maven.apache.org/maven2</url>
|
||||
</pluginRepository>
|
||||
</pluginRepositories>
|
||||
</profile>
|
||||
</profiles>
|
||||
|
||||
<!-- 激活 profile -->
|
||||
<activeProfiles>
|
||||
<activeProfile>repos</activeProfile>
|
||||
</activeProfiles>
|
||||
|
||||
</settings>
|
||||
207
docker/docker-build.sh
Normal file
207
docker/docker-build.sh
Normal file
@@ -0,0 +1,207 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# DockerContext: orion-visor/docker
|
||||
|
||||
# 加载项目构建
|
||||
source ./project-build.sh "$@"
|
||||
|
||||
# 版本号
|
||||
version=2.4.2
|
||||
# 是否推送镜像
|
||||
push_image=false
|
||||
# 是否构建 latest
|
||||
latest_image=false
|
||||
# 是否本地构建
|
||||
locally_build=false
|
||||
# 备份后缀
|
||||
backup_suffix=".bak"
|
||||
# 镜像命名空间
|
||||
namespace="registry.cn-hangzhou.aliyuncs.com/orionsec"
|
||||
|
||||
# 解析命令行参数
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
-l|--locally)
|
||||
locally_build=true
|
||||
shift
|
||||
;;
|
||||
-latest|--latest-image)
|
||||
latest_image=true
|
||||
shift
|
||||
;;
|
||||
-push|--push-image)
|
||||
push_image=true
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# 要处理的 Dockerfile 列表及对应的镜像名称
|
||||
declare -A images=(
|
||||
["./ui/Dockerfile"]="orion-visor-ui"
|
||||
["./service/Dockerfile"]="orion-visor-service"
|
||||
["./mysql/Dockerfile"]="orion-visor-mysql"
|
||||
["./redis/Dockerfile"]="orion-visor-redis"
|
||||
["./adminer/Dockerfile"]="orion-visor-adminer"
|
||||
["./guacd/Dockerfile"]="orion-visor-guacd"
|
||||
)
|
||||
|
||||
# 准备 service jar
|
||||
function prepare_app_jar() {
|
||||
local source_file="../orion-visor-launch/target/orion-visor-launch.jar"
|
||||
local target_file="./service/orion-visor-launch.jar"
|
||||
if [ ! -f "$target_file" ]; then
|
||||
echo "警告: $target_file 不存在, 正在尝试从 $source_file 复制..."
|
||||
if [ -f "$source_file" ]; then
|
||||
cp "$source_file" "$target_file"
|
||||
echo "已成功复制 $source_file 至 $target_file"
|
||||
else
|
||||
echo "错误: $source_file 不存在, 无法继续构建."
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "$target_file 已存在, 无需复制."
|
||||
fi
|
||||
}
|
||||
|
||||
# 准备前端 dist 目录
|
||||
function prepare_dist_directory() {
|
||||
local source_dir="../orion-visor-ui/dist"
|
||||
local target_dir="./ui/dist"
|
||||
if [ ! -d "$target_dir" ]; then
|
||||
echo "警告: $target_dir 不存在, 正在尝试从 $source_dir 复制..."
|
||||
if [ -d "$source_dir" ]; then
|
||||
cp -r "$source_dir" "$target_dir"
|
||||
echo "已成功复制 $source_dir 至 $target_dir"
|
||||
else
|
||||
echo "错误: $source_dir 不存在, 无法继续构建."
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "$target_dir 已存在, 无需复制."
|
||||
fi
|
||||
}
|
||||
|
||||
# 准备 mysql sql 目录
|
||||
function prepare_sql_directory() {
|
||||
local source_dir="../sql"
|
||||
local target_dir="./mysql/sql"
|
||||
if [ ! -d "$target_dir" ]; then
|
||||
echo "警告: $target_dir 不存在, 正在尝试从 $source_dir 复制..."
|
||||
if [ -d $source_dir ]; then
|
||||
cp -r $source_dir "$target_dir"
|
||||
echo "已成功复制 ../sql 至 $target_dir"
|
||||
else
|
||||
echo "错误: $source_dir 不存在!根据预期它应该存在, 请确认路径或项目结构是否正确"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "$target_dir 已存在, 无需复制."
|
||||
fi
|
||||
}
|
||||
|
||||
# 修改 Dockerfile 前的备份
|
||||
function modify_dockerfiles() {
|
||||
if [ "$locally_build" = false ]; then
|
||||
echo "跳过 Dockerfile 修改"
|
||||
return
|
||||
fi
|
||||
echo "正在备份并修改 Dockerfile..."
|
||||
for file in "${!images[@]}"; do
|
||||
if [ -f "$file" ]; then
|
||||
echo "备份并修改: $file"
|
||||
cp "$file" "$file$backup_suffix"
|
||||
sed -i 's/--platform=\$BUILDPLATFORM//g' "$file"
|
||||
else
|
||||
echo "文件不存在 -> $file"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
# 恢复原始 Dockerfile
|
||||
function restore_dockerfiles() {
|
||||
if [ "$locally_build" = false ]; then
|
||||
return
|
||||
fi
|
||||
echo "开始恢复 Dockerfile"
|
||||
for file in "${!images[@]}"; do
|
||||
if [ -f "$file$backup_suffix" ]; then
|
||||
echo "恢复: $file"
|
||||
rm -rf "$file"
|
||||
mv "$file$backup_suffix" "$file"
|
||||
fi
|
||||
done
|
||||
echo "Dockerfile 已恢复为原始版本"
|
||||
}
|
||||
|
||||
# 构建镜像
|
||||
function build_images() {
|
||||
echo "构建镜像开始..."
|
||||
for dockerfile in "${!images[@]}"; do
|
||||
image_name="${images[$dockerfile]}"
|
||||
echo "Building $image_name with version $version."
|
||||
# 构建 Docker 镜像
|
||||
docker build -f "$dockerfile" -t "${image_name}:${version}" -t "${namespace}/${image_name}:${version}" .
|
||||
# 添加 latest 标签
|
||||
if [ "$latest_image" = true ]; then
|
||||
echo "Tag $image_name with latest version."
|
||||
docker tag "${image_name}:${version}" "${image_name}:latest"
|
||||
docker tag "${namespace}/${image_name}:${version}" "${namespace}/${image_name}:latest"
|
||||
fi
|
||||
done
|
||||
echo "构建镜像结束..."
|
||||
}
|
||||
|
||||
# 推送镜像
|
||||
function push_image_to_registry() {
|
||||
if [ "$push_image" = true ]; then
|
||||
echo "推送镜像开始..."
|
||||
for image_name in "${images[@]}"; do
|
||||
# 推送版本
|
||||
docker push "${namespace}/${image_name}:${version}"
|
||||
# 推送 latest
|
||||
if [ "latest_image" = true ]; then
|
||||
docker push "${namespace}/${image_name}:latest"
|
||||
fi
|
||||
done
|
||||
echo "推送镜像结束..."
|
||||
fi
|
||||
}
|
||||
|
||||
# 构建项目-service
|
||||
if [ "$build_service" = true ]; then
|
||||
run_build_service
|
||||
fi
|
||||
|
||||
# 构建项目-ui
|
||||
if [ "$build_ui" = true ]; then
|
||||
run_build_ui
|
||||
fi
|
||||
|
||||
# 检查资源
|
||||
echo "正在检查并准备必要的构建资源..."
|
||||
prepare_app_jar
|
||||
prepare_dist_directory
|
||||
prepare_sql_directory
|
||||
echo "所有前置资源已准备完毕"
|
||||
|
||||
# 修改镜像文件
|
||||
modify_dockerfiles
|
||||
|
||||
# 设置异常捕获, 确保失败时恢复 Dockerfile
|
||||
trap 'restore_dockerfiles; echo "构建失败, 已恢复原始 Dockerfile"; exit 1' ERR INT
|
||||
|
||||
# 构建镜像
|
||||
build_images
|
||||
|
||||
# 推送镜像
|
||||
push_image_to_registry
|
||||
|
||||
# 恢复原始 Dockerfile
|
||||
restore_dockerfiles
|
||||
trap - ERR INT
|
||||
echo "构建完成"
|
||||
@@ -1,10 +1,17 @@
|
||||
FROM --platform=$BUILDPLATFORM guacamole/guacd:1.6.0
|
||||
|
||||
USER root
|
||||
|
||||
# 系统时区
|
||||
ARG TZ=Asia/Shanghai
|
||||
# 设置时区
|
||||
RUN ln -sf /usr/share/zoneinfo/${TZ} /etc/localtime && \
|
||||
echo '${TZ}' > /etc/timezone
|
||||
|
||||
# 添加包 & 设置时区
|
||||
RUN \
|
||||
sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories && \
|
||||
apk update && \
|
||||
apk add --no-cache tzdata && \
|
||||
ln -sf /usr/share/zoneinfo/${TZ} /etc/localtime && \
|
||||
echo "${TZ}" > /etc/timezone
|
||||
|
||||
# 创建所需目录
|
||||
RUN mkdir -p /home/guacd/drive /usr/share/guacd/drive
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
FROM --platform=$BUILDPLATFORM mysql:8.0.28
|
||||
|
||||
# 系统时区
|
||||
ARG TZ=Asia/Shanghai
|
||||
|
||||
# 设置时区
|
||||
RUN ln -sf /usr/share/zoneinfo/${TZ} /etc/localtime && \
|
||||
echo '${TZ}' > /etc/timezone
|
||||
# 复制配置
|
||||
COPY ./docker/mysql/my.cnf /etc/mysql/conf.d/my.cnf
|
||||
echo "${TZ}" > /etc/timezone
|
||||
|
||||
# 复制配置文件
|
||||
COPY ./mysql/my.cnf /etc/mysql/conf.d/my.cnf
|
||||
|
||||
# 复制初始化脚本
|
||||
COPY ./sql /tmp
|
||||
# 设置初始化脚本
|
||||
RUN cat /tmp/init-1-schema-databases.sql >> /tmp/init.sql && \
|
||||
cat /tmp/init-2-schema-tables.sql >> /tmp/init.sql && \
|
||||
cat /tmp/init-3-schema-quartz.sql >> /tmp/init.sql && \
|
||||
cat /tmp/init-4-data.sql >> /tmp/init.sql && \
|
||||
cp /tmp/init.sql /docker-entrypoint-initdb.d
|
||||
COPY ./mysql/sql/init-*.sql /docker-entrypoint-initdb.d/
|
||||
|
||||
125
docker/project-build.sh
Normal file
125
docker/project-build.sh
Normal file
@@ -0,0 +1,125 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# DockerContext: orion-visor
|
||||
|
||||
# 版本号
|
||||
version=2.4.2
|
||||
# 是否构建 service
|
||||
export build_service=false
|
||||
# 是否构建 ui
|
||||
export build_ui=false
|
||||
|
||||
# 解析命令行参数
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
-service|--build-service)
|
||||
export build_service=true
|
||||
;;
|
||||
-ui|--build-ui)
|
||||
export build_ui=true
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# 执行构建 service
|
||||
function run_build_service() {
|
||||
echo "开始执行 service 构建流程..."
|
||||
|
||||
local builder_dockerfile="./builder/Dockerfile.service"
|
||||
local builder_image="orion-visor-service-builder"
|
||||
local builder_container="orion-visor-service-builder-ctn"
|
||||
local builder_output="/build/orion-visor-launch/target/orion-visor-launch.jar"
|
||||
local target_dir="../orion-visor-launch/target"
|
||||
local target_jar="$target_dir/orion-visor-launch.jar"
|
||||
|
||||
# 确保目标目录存在
|
||||
if [ ! -d "$target_dir" ]; then
|
||||
echo "创建目标目录: $target_dir"
|
||||
mkdir -p "$target_dir"
|
||||
else
|
||||
# 如果 jar 已存在, 先删除
|
||||
if [ -f "$target_jar" ]; then
|
||||
echo "删除已有文件: $target_jar"
|
||||
rm -f "$target_jar"
|
||||
fi
|
||||
fi
|
||||
|
||||
# 清理旧容器
|
||||
local container_id=$(docker ps -a -f "name=$builder_container" --format "{{.ID}}")
|
||||
if [ -n "$container_id" ]; then
|
||||
echo "删除旧容器: $builder_container"
|
||||
docker rm -f "$container_id"
|
||||
fi
|
||||
|
||||
# 构建构建镜像
|
||||
echo "正在构建 service builder image..."
|
||||
docker build \
|
||||
-f "$builder_dockerfile" \
|
||||
-t "$builder_image:$version" ../
|
||||
|
||||
# 创建一个临时容器用于拷贝文件
|
||||
echo "创建临时容器以提取 jar 文件..."
|
||||
docker create --name "$builder_container" "$builder_image:$version" > /dev/null
|
||||
|
||||
# 拷贝构建好的 jar 文件到目标路径
|
||||
echo "正在从容器中拷贝 jar 文件..."
|
||||
docker cp "$builder_container:$builder_output" "$target_jar"
|
||||
|
||||
# 清理临时容器
|
||||
docker rm -f "$builder_container" > /dev/null
|
||||
echo "后端构建完成, jar 文件已保存至: $target_jar"
|
||||
}
|
||||
|
||||
# 执行构建 ui
|
||||
function run_build_ui() {
|
||||
echo "开始执行 ui 构建流程..."
|
||||
|
||||
local builder_dockerfile="./builder/Dockerfile.ui"
|
||||
local builder_image="orion-visor-ui-builder"
|
||||
local builder_container="orion-visor-ui-builder-ctn"
|
||||
local builder_output="/build/dist"
|
||||
local target_dir="../orion-visor-ui/dist"
|
||||
|
||||
# 如果 dist 已存在, 先删除
|
||||
if [ -d "$target_dir" ]; then
|
||||
echo "删除已有目录: $target_dir"
|
||||
rm -rf "$target_dir"
|
||||
fi
|
||||
|
||||
# 清理旧容器
|
||||
local container_id=$(docker ps -a -f "name=$builder_container" --format "{{.ID}}")
|
||||
if [ -n "$container_id" ]; then
|
||||
echo "删除旧容器: $builder_container"
|
||||
docker rm -f "$container_id"
|
||||
fi
|
||||
|
||||
# 构建前端镜像
|
||||
echo "正在构建 ui builder image..."
|
||||
docker build \
|
||||
-f "$builder_dockerfile" \
|
||||
-t "$builder_image:$version" ../
|
||||
|
||||
# 创建临时容器用于拷贝文件
|
||||
echo "创建临时容器以提取 dist 文件..."
|
||||
docker create --name "$builder_container" "$builder_image:$version" > /dev/null
|
||||
|
||||
# 拷贝 dist 目录
|
||||
echo "正在从容器中拷贝 dist 文件..."
|
||||
docker cp "$builder_container:$builder_output" "$target_dir"
|
||||
|
||||
# 清理临时容器
|
||||
docker rm "$builder_container" > /dev/null
|
||||
|
||||
echo "前端构建完成, dist 已保存至: $target_dir"
|
||||
}
|
||||
|
||||
# 构建项目-service
|
||||
if [ "$build_service" = true ]; then
|
||||
run_build_service
|
||||
fi
|
||||
|
||||
# 构建项目-ui
|
||||
if [ "$build_ui" = true ]; then
|
||||
run_build_ui
|
||||
fi
|
||||
@@ -1,15 +1,22 @@
|
||||
FROM --platform=$BUILDPLATFORM redis:6.0.16-alpine
|
||||
|
||||
WORKDIR /data
|
||||
|
||||
# 系统时区
|
||||
ARG TZ=Asia/Shanghai
|
||||
# 添加包
|
||||
|
||||
# 添加包 & 设置时区
|
||||
RUN \
|
||||
sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories && \
|
||||
apk update && \
|
||||
apk add tzdata
|
||||
# 设置时区
|
||||
RUN ln -sf /usr/share/zoneinfo/${TZ} /etc/localtime && \
|
||||
echo '${TZ}' > /etc/timezone
|
||||
# redis 配置
|
||||
COPY ./docker/redis/redis.conf /tmp
|
||||
RUN cat /tmp/redis.conf > /usr/local/redis.conf
|
||||
apk add --no-cache tzdata && \
|
||||
ln -sf /usr/share/zoneinfo/${TZ} /etc/localtime && \
|
||||
echo "${TZ}" > /etc/timezone && \
|
||||
rm -rf /var/cache/apk/* && \
|
||||
rm -f /usr/local/redis.conf
|
||||
|
||||
# 复制配置文件
|
||||
COPY ./redis/redis.conf /usr/local/redis.conf
|
||||
|
||||
# 启动 Redis 并加载自定义配置
|
||||
CMD ["redis-server", "/usr/local/redis.conf"]
|
||||
@@ -1,36 +1,24 @@
|
||||
# 第一阶段:Maven构建阶段
|
||||
FROM --platform=$BUILDPLATFORM maven:3.9.10-eclipse-temurin-8-alpine AS builder
|
||||
|
||||
# 设置阿里云镜像加速
|
||||
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories
|
||||
|
||||
# 复制POM文件先进行依赖下载(利用Docker缓存)
|
||||
WORKDIR /build
|
||||
COPY . .
|
||||
RUN mvn dependency:go-offline
|
||||
|
||||
# 构建
|
||||
RUN mvn clean package -DskipTests
|
||||
|
||||
FROM --platform=$BUILDPLATFORM openjdk:8-jdk-alpine
|
||||
|
||||
USER root
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# 系统时区
|
||||
ARG TZ=Asia/Shanghai
|
||||
# 添加包
|
||||
|
||||
# 添加包 & 设置时区
|
||||
RUN \
|
||||
sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories && \
|
||||
apk update && \
|
||||
apk add curl && \
|
||||
apk add udev && \
|
||||
apk add tzdata && \
|
||||
apk add dmidecode
|
||||
# 设置时区
|
||||
RUN ln -sf /usr/share/zoneinfo/${TZ} /etc/localtime && \
|
||||
echo '${TZ}' > /etc/timezone
|
||||
ln -sf /usr/share/zoneinfo/${TZ} /etc/localtime && \
|
||||
echo "${TZ}" > /etc/timezone
|
||||
|
||||
# 从构建阶段复制jar包
|
||||
COPY --from=builder /build/orion-visor-launch/target/orion-visor-launch.jar /app/app.jar
|
||||
# 复制 jar 包
|
||||
COPY ./service/orion-visor-launch.jar /app/app.jar
|
||||
|
||||
# 启动
|
||||
CMD ["java", "-jar", "/app/app.jar"]
|
||||
CMD ["java", "-jar", "/app/app.jar"]
|
||||
|
||||
@@ -1,40 +1,23 @@
|
||||
FROM --platform=$BUILDPLATFORM node:18-alpine AS builder
|
||||
|
||||
# 设置阿里云镜像加速
|
||||
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories
|
||||
|
||||
# 安装pnpm
|
||||
RUN corepack enable && corepack prepare pnpm@latest --activate
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# 复制项目文件(包括package.json等)
|
||||
COPY ./orion-visor-ui/package.json ./orion-visor-ui/pnpm-lock.yaml* ./
|
||||
|
||||
# 安装依赖(利用Docker缓存层)
|
||||
RUN pnpm install --frozen-lockfile
|
||||
|
||||
# 复制源代码
|
||||
COPY ./orion-visor-ui/ .
|
||||
|
||||
# 构建项目
|
||||
RUN pnpm build
|
||||
|
||||
FROM --platform=$BUILDPLATFORM nginx:alpine
|
||||
|
||||
# 系统时区
|
||||
ARG TZ=Asia/Shanghai
|
||||
# 添加包
|
||||
|
||||
# 添加包 & 设置时区
|
||||
RUN \
|
||||
sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories && \
|
||||
apk update && \
|
||||
apk add tzdata
|
||||
# 设置时区
|
||||
RUN ln -sf /usr/share/zoneinfo/${TZ} /etc/localtime && \
|
||||
echo '${TZ}' > /etc/timezone
|
||||
# 删除原 nginx 配置
|
||||
RUN rm -rf /etc/nginx/conf.d/*
|
||||
apk add --no-cache tzdata && \
|
||||
ln -sf /usr/share/zoneinfo/${TZ} /etc/localtime && \
|
||||
echo "${TZ}" > /etc/timezone && \
|
||||
rm -rf /var/cache/apk/* && \
|
||||
rm -rf /etc/nginx/conf.d/*
|
||||
|
||||
# 复制包
|
||||
COPY --from=builder /app/dist /usr/share/nginx/html
|
||||
COPY ./docker/ui/nginx.conf /etc/nginx/conf.d
|
||||
COPY ./ui/dist /usr/share/nginx/html
|
||||
|
||||
# 复制配置
|
||||
COPY ./ui/nginx.conf /etc/nginx/conf.d
|
||||
|
||||
# 启动
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
5
docs/README.md
Normal file
5
docs/README.md
Normal file
@@ -0,0 +1,5 @@
|
||||
## 文档已迁移至网页端
|
||||
|
||||
* https://visor.dromara.org
|
||||
* https://visor.dromara.org.cn
|
||||
* https://visor.orionsec.cn
|
||||
@@ -1,4 +1,4 @@
|
||||
#/bin/bash
|
||||
#!/bin/bash
|
||||
git clean -df
|
||||
git reset --hard HEAD
|
||||
git pull
|
||||
@@ -36,7 +36,7 @@ public interface AppConst extends OrionConst {
|
||||
/**
|
||||
* 同 ${orion.version} 迭代时候需要手动更改
|
||||
*/
|
||||
String VERSION = "2.4.1";
|
||||
String VERSION = "2.4.2";
|
||||
|
||||
/**
|
||||
* 同 ${spring.application.name}
|
||||
|
||||
@@ -51,9 +51,6 @@ public interface Const extends cn.orionsec.kit.lang.constant.Const, FieldConst,
|
||||
|
||||
String SYSTEM_USERNAME = "system";
|
||||
|
||||
// FIXME KIT
|
||||
String ADMINISTRATOR = "Administrator";
|
||||
|
||||
Long ALL_HOST_ID = -1L;
|
||||
|
||||
int BATCH_COUNT = 500;
|
||||
|
||||
@@ -22,16 +22,14 @@
|
||||
*/
|
||||
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 {
|
||||
public interface CustomHeaderConst {
|
||||
|
||||
String APP_VERSION = "X-App-Version";
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* 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.session.config;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
|
||||
/**
|
||||
* VNC 连接参数
|
||||
*
|
||||
* @author Jiahang Li
|
||||
* @version 1.0.0
|
||||
* @since 2025/4/1 16:57
|
||||
*/
|
||||
@Data
|
||||
@SuperBuilder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Schema(name = "VncConnectConfig", description = "VNC 连接参数")
|
||||
public class VncConnectConfig extends BaseConnectConfig {
|
||||
|
||||
@Schema(description = "低带宽模式")
|
||||
private Boolean lowBandwidthMode;
|
||||
|
||||
@Schema(description = "交换红蓝")
|
||||
private Boolean swapRedBlue;
|
||||
|
||||
@Schema(description = "时区")
|
||||
private String timezone;
|
||||
|
||||
@Schema(description = "剪切板编码")
|
||||
private String clipboardEncoding;
|
||||
|
||||
}
|
||||
@@ -110,7 +110,10 @@ public class SessionStores {
|
||||
}
|
||||
}
|
||||
// 超时时间
|
||||
session.timeout(config.getTimeout());
|
||||
Integer timeout = config.getTimeout();
|
||||
if (timeout != null) {
|
||||
session.timeout(timeout);
|
||||
}
|
||||
return session;
|
||||
}
|
||||
|
||||
|
||||
@@ -14,11 +14,11 @@
|
||||
<url>https://github.com/dromara/orion-visor</url>
|
||||
|
||||
<properties>
|
||||
<revision>2.4.1</revision>
|
||||
<revision>2.4.2</revision>
|
||||
<spring.boot.version>2.7.17</spring.boot.version>
|
||||
<spring.boot.admin.version>2.7.15</spring.boot.admin.version>
|
||||
<flatten.maven.plugin.version>1.5.0</flatten.maven.plugin.version>
|
||||
<orion.kit.version>2.0.1</orion.kit.version>
|
||||
<orion.kit.version>2.0.2</orion.kit.version>
|
||||
<aspectj.version>1.9.7</aspectj.version>
|
||||
<lombok.version>1.18.26</lombok.version>
|
||||
<springdoc.version>1.6.15</springdoc.version>
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
<artifactId>netty-all</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- test redis noRedis -->
|
||||
<!-- test redis MockRedis -->
|
||||
<dependency>
|
||||
<groupId>com.github.fppt</groupId>
|
||||
<artifactId>jedis-mock</artifactId>
|
||||
|
||||
@@ -38,17 +38,17 @@ import java.net.InetAddress;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
* noRedis 配置
|
||||
* MockRedis
|
||||
* 仅用于本地调试无 redis 的情况
|
||||
*
|
||||
* @author Jiahang Li
|
||||
* @version 1.0.0
|
||||
* @since 2024/12/26 10:02
|
||||
*/
|
||||
@ConditionalOnProperty(value = "no.redis", havingValue = "true")
|
||||
@ConditionalOnProperty(value = "spring.redis.mock", havingValue = "true")
|
||||
@AutoConfiguration
|
||||
@AutoConfigureOrder(AutoConfigureOrderConst.FRAMEWORK_REDIS - 10)
|
||||
public class OrionNoRedisAutoConfiguration {
|
||||
public class OrionMockRedisAutoConfiguration {
|
||||
|
||||
/**
|
||||
* @return mocked redis server
|
||||
@@ -22,6 +22,7 @@
|
||||
*/
|
||||
package org.dromara.visor.framework.redis.configuration;
|
||||
|
||||
import cn.orionsec.kit.lang.define.cache.key.CacheKeyDefine;
|
||||
import org.dromara.visor.common.constant.AutoConfigureOrderConst;
|
||||
import org.dromara.visor.common.interfaces.Locker;
|
||||
import org.dromara.visor.common.utils.LockerUtils;
|
||||
@@ -31,6 +32,7 @@ import org.dromara.visor.framework.redis.core.utils.RedisUtils;
|
||||
import org.redisson.api.RedissonClient;
|
||||
import org.redisson.config.SingleServerConfig;
|
||||
import org.redisson.spring.starter.RedissonAutoConfigurationCustomizer;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.AutoConfigureOrder;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
@@ -101,5 +103,15 @@ public class OrionRedisAutoConfiguration {
|
||||
return redisLocker;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置 redis 数据版本
|
||||
*
|
||||
* @param dataVersion dataVersion
|
||||
*/
|
||||
@Value("${spring.redis.data-version}")
|
||||
public void setDataVersion(String dataVersion) {
|
||||
CacheKeyDefine.setGlobalPrefix("v" + dataVersion + ":");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -26,9 +26,15 @@
|
||||
"defaultValue": "16"
|
||||
},
|
||||
{
|
||||
"name": "no.redis",
|
||||
"name": "spring.redis.data-version",
|
||||
"type": "java.lang.String",
|
||||
"description": "redis 数据版本.",
|
||||
"defaultValue": "1"
|
||||
},
|
||||
{
|
||||
"name": "spring.redis.mock",
|
||||
"type": "java.lang.Boolean",
|
||||
"description": "是否无 redis.",
|
||||
"description": "是否使用 mock redis, 一般用于无 redis 调试时使用.",
|
||||
"defaultValue": false
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
org.dromara.visor.framework.redis.configuration.OrionNoRedisAutoConfiguration
|
||||
org.dromara.visor.framework.redis.configuration.OrionMockRedisAutoConfiguration
|
||||
org.dromara.visor.framework.redis.configuration.OrionRedisAutoConfiguration
|
||||
org.dromara.visor.framework.redis.configuration.OrionCacheAutoConfiguration
|
||||
@@ -13,6 +13,9 @@ spring:
|
||||
host: ${REDIS_HOST:127.0.0.1}
|
||||
port: ${REDIS_PORT:6379}
|
||||
password: ${REDIS_PASSWORD:Data@123456}
|
||||
database: ${REDIS_DATABASE:10}
|
||||
data-version: ${REDIS_DATA_VERSION:1}
|
||||
mock: false
|
||||
redisson:
|
||||
threads: 2
|
||||
netty-threads: 2
|
||||
@@ -40,6 +43,3 @@ mybatis-plus:
|
||||
configuration:
|
||||
# 日志打印
|
||||
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
|
||||
|
||||
no:
|
||||
redis: false
|
||||
|
||||
@@ -24,6 +24,8 @@ spring:
|
||||
host: ${REDIS_HOST:127.0.0.1}
|
||||
port: ${REDIS_PORT:6379}
|
||||
password: ${REDIS_PASSWORD:Data@123456}
|
||||
database: ${REDIS_DATABASE:0}
|
||||
data-version: ${REDIS_DATA_VERSION:1}
|
||||
redisson:
|
||||
threads: 4
|
||||
netty-threads: 4
|
||||
|
||||
@@ -39,20 +39,15 @@ import java.util.function.Function;
|
||||
*/
|
||||
public class ReplaceVersion {
|
||||
|
||||
private static final String TARGET_VERSION = "2.4.0";
|
||||
private static final String TARGET_VERSION = "2.4.1";
|
||||
|
||||
private static final String REPLACE_VERSION = "2.4.1";
|
||||
private static final String REPLACE_VERSION = "2.4.2";
|
||||
|
||||
private static final String PATH = new File("").getAbsolutePath();
|
||||
|
||||
private static final String[] DOCKER_FILES = new String[]{
|
||||
"docker/push.sh",
|
||||
"docker/adminer/build.sh",
|
||||
"docker/mysql/build.sh",
|
||||
"docker/redis/build.sh",
|
||||
"docker/guacd/build.sh",
|
||||
"docker/service/build.sh",
|
||||
"docker/ui/build.sh",
|
||||
"docker/docker-build.sh",
|
||||
"docker/project-build.sh",
|
||||
"docker-compose.yml",
|
||||
"docker-compose-testing.yml"
|
||||
};
|
||||
|
||||
@@ -24,6 +24,7 @@ package org.dromara.visor.module.asset.api;
|
||||
|
||||
import org.dromara.visor.common.session.config.RdpConnectConfig;
|
||||
import org.dromara.visor.common.session.config.SshConnectConfig;
|
||||
import org.dromara.visor.common.session.config.VncConnectConfig;
|
||||
import org.dromara.visor.module.asset.entity.dto.host.HostDTO;
|
||||
|
||||
/**
|
||||
@@ -87,4 +88,30 @@ public interface HostConnectApi {
|
||||
*/
|
||||
RdpConnectConfig getRdpConnectConfig(HostDTO host, Long userId);
|
||||
|
||||
/**
|
||||
* 获取 VNC 连接配置
|
||||
*
|
||||
* @param hostId hostId
|
||||
* @return session
|
||||
*/
|
||||
VncConnectConfig getVncConnectConfig(Long hostId);
|
||||
|
||||
/**
|
||||
* 使用用户配置获取 VNC 连接配置
|
||||
*
|
||||
* @param hostId hostId
|
||||
* @param userId userId
|
||||
* @return session
|
||||
*/
|
||||
VncConnectConfig getVncConnectConfig(Long hostId, Long userId);
|
||||
|
||||
/**
|
||||
* 使用用户配置获取 VNC 连接配置
|
||||
*
|
||||
* @param host host
|
||||
* @param userId userId
|
||||
* @return session
|
||||
*/
|
||||
VncConnectConfig getVncConnectConfig(HostDTO host, Long userId);
|
||||
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ public class HostSshConfigDTO implements GenericsDataModel, UpdatePasswordAction
|
||||
private Long keyId;
|
||||
|
||||
@NotNull
|
||||
@Min(value = 1)
|
||||
@Min(value = 0)
|
||||
@Max(value = 100000)
|
||||
@Schema(description = "连接超时时间")
|
||||
private Integer connectTimeout;
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
* 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.asset.entity.dto.host;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.dromara.visor.common.handler.data.model.GenericsDataModel;
|
||||
import org.dromara.visor.common.security.UpdatePasswordAction;
|
||||
|
||||
import javax.validation.constraints.*;
|
||||
|
||||
/**
|
||||
* 主机 VNC 配置
|
||||
*
|
||||
* @author Jiahang Li
|
||||
* @version 1.0.0
|
||||
* @since 2023/9/13 16:18
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(name = "HostVncConfigDTO", description = "主机 VNC 配置业务对象")
|
||||
public class HostVncConfigDTO implements GenericsDataModel, UpdatePasswordAction {
|
||||
|
||||
@NotNull
|
||||
@Min(value = 1)
|
||||
@Max(value = 65535)
|
||||
@Schema(description = "主机端口")
|
||||
private Integer port;
|
||||
|
||||
@Size(max = 128)
|
||||
@Schema(description = "用户名")
|
||||
private String username;
|
||||
|
||||
@NotBlank
|
||||
@Size(max = 12)
|
||||
@Schema(description = "认证方式")
|
||||
private String authType;
|
||||
|
||||
@Schema(description = "密码")
|
||||
private String password;
|
||||
|
||||
@Schema(description = "身份id")
|
||||
private Long identityId;
|
||||
|
||||
@Schema(description = "无用户名")
|
||||
private Boolean noUsername;
|
||||
|
||||
@Schema(description = "无密码")
|
||||
private Boolean noPassword;
|
||||
|
||||
@Schema(description = "时区")
|
||||
private String timezone;
|
||||
|
||||
@Schema(description = "剪切板编码")
|
||||
private String clipboardEncoding;
|
||||
|
||||
@Schema(description = "是否使用新密码 仅参数")
|
||||
private Boolean useNewPassword;
|
||||
|
||||
@Schema(description = "是否已设置密码 仅返回")
|
||||
private Boolean hasPassword;
|
||||
|
||||
}
|
||||
@@ -27,6 +27,7 @@ import lombok.AllArgsConstructor;
|
||||
import org.dromara.visor.common.constant.Const;
|
||||
import org.dromara.visor.module.asset.entity.dto.host.HostRdpConfigDTO;
|
||||
import org.dromara.visor.module.asset.entity.dto.host.HostSshConfigDTO;
|
||||
import org.dromara.visor.module.asset.entity.dto.host.HostVncConfigDTO;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
@@ -54,6 +55,11 @@ public enum HostTypeEnum {
|
||||
*/
|
||||
RDP(HostRdpConfigDTO.class),
|
||||
|
||||
/**
|
||||
* VNC
|
||||
*/
|
||||
VNC(HostVncConfigDTO.class),
|
||||
|
||||
;
|
||||
|
||||
private final Class<?> clazz;
|
||||
|
||||
@@ -25,6 +25,7 @@ package org.dromara.visor.module.asset.api.impl;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.dromara.visor.common.session.config.RdpConnectConfig;
|
||||
import org.dromara.visor.common.session.config.SshConnectConfig;
|
||||
import org.dromara.visor.common.session.config.VncConnectConfig;
|
||||
import org.dromara.visor.module.asset.api.HostConnectApi;
|
||||
import org.dromara.visor.module.asset.convert.HostProviderConvert;
|
||||
import org.dromara.visor.module.asset.entity.dto.host.HostDTO;
|
||||
@@ -77,4 +78,19 @@ public class HostConnectApiImpl implements HostConnectApi {
|
||||
return hostConnectService.getRdpConnectConfig(HostProviderConvert.MAPPER.to(host), userId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public VncConnectConfig getVncConnectConfig(Long hostId) {
|
||||
return hostConnectService.getVncConnectConfig(hostId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public VncConnectConfig getVncConnectConfig(Long hostId, Long userId) {
|
||||
return hostConnectService.getVncConnectConfig(hostId, userId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public VncConnectConfig getVncConnectConfig(HostDTO host, Long userId) {
|
||||
return hostConnectService.getVncConnectConfig(HostProviderConvert.MAPPER.to(host), userId);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -49,6 +49,11 @@ public enum HostConfigStrategyEnum implements GenericsStrategyDefinition {
|
||||
*/
|
||||
RDP(HostRdpConfigStrategy.class),
|
||||
|
||||
/**
|
||||
* VNC
|
||||
*/
|
||||
VNC(HostVncConfigStrategy.class),
|
||||
|
||||
;
|
||||
|
||||
private final Class<? extends GenericsDataStrategy<? extends GenericsDataModel>> strategyClass;
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
/*
|
||||
* 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.asset.handler.host.config;
|
||||
|
||||
import cn.orionsec.kit.lang.utils.Booleans;
|
||||
import cn.orionsec.kit.lang.utils.Strings;
|
||||
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.asset.dao.HostIdentityDAO;
|
||||
import org.dromara.visor.module.asset.entity.domain.HostIdentityDO;
|
||||
import org.dromara.visor.module.asset.entity.dto.host.HostVncConfigDTO;
|
||||
import org.dromara.visor.module.asset.enums.HostAuthTypeEnum;
|
||||
import org.dromara.visor.module.asset.enums.HostIdentityTypeEnum;
|
||||
import org.dromara.visor.module.asset.enums.HostTypeEnum;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
|
||||
/**
|
||||
* 主机 VNC 配置策略
|
||||
*
|
||||
* @author Jiahang Li
|
||||
* @version 1.0.0
|
||||
* @since 2023/9/19 14:26
|
||||
*/
|
||||
@Component
|
||||
public class HostVncConfigStrategy extends AbstractHostConfigStrategy<HostVncConfigDTO> {
|
||||
|
||||
@Resource
|
||||
private HostIdentityDAO hostIdentityDAO;
|
||||
|
||||
public HostVncConfigStrategy() {
|
||||
super(HostVncConfigDTO.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public HostVncConfigDTO getDefault() {
|
||||
return HostVncConfigDTO.builder()
|
||||
.port(5900)
|
||||
.username(Const.ROOT)
|
||||
.authType(HostAuthTypeEnum.PASSWORD.name())
|
||||
.noUsername(false)
|
||||
.noPassword(false)
|
||||
.clipboardEncoding(Const.UTF_8)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void preValid(HostVncConfigDTO model) {
|
||||
// 验证剪切板编码格式
|
||||
String clipboardEncoding = model.getClipboardEncoding();
|
||||
if (!Strings.isBlank(clipboardEncoding)) {
|
||||
this.validCharset(clipboardEncoding);
|
||||
}
|
||||
// 检查主机身份是否存在
|
||||
Long identityId = model.getIdentityId();
|
||||
if (identityId != null) {
|
||||
HostIdentityDO identity = Valid.notNull(hostIdentityDAO.selectById(identityId), ErrorMessage.IDENTITY_ABSENT);
|
||||
Valid.eq(HostIdentityTypeEnum.PASSWORD.name(), identity.getType(), ErrorMessage.CHECK_IDENTITY_PASSWORD);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void valid(HostVncConfigDTO model) {
|
||||
// 验证填充后的参数
|
||||
Valid.valid(model);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateFill(HostVncConfigDTO beforeModel, HostVncConfigDTO afterModel) {
|
||||
// 无密码设置认证方式为密码验证
|
||||
if (Booleans.isTrue(afterModel.getNoPassword())) {
|
||||
afterModel.setAuthType(HostAuthTypeEnum.PASSWORD.name());
|
||||
}
|
||||
// 加密密码
|
||||
this.checkEncryptPassword(afterModel.getAuthType(), beforeModel, afterModel);
|
||||
afterModel.setHasPassword(null);
|
||||
afterModel.setUseNewPassword(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public HostVncConfigDTO parse(String serialModel) {
|
||||
return HostTypeEnum.VNC.parse(serialModel);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void toView(HostVncConfigDTO model) {
|
||||
if (model == null) {
|
||||
return;
|
||||
}
|
||||
model.setHasPassword(Strings.isNotBlank(model.getPassword()));
|
||||
model.setPassword(null);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -27,10 +27,7 @@ import lombok.Getter;
|
||||
import org.dromara.visor.common.handler.data.GenericsStrategyDefinition;
|
||||
import org.dromara.visor.common.handler.data.model.GenericsDataModel;
|
||||
import org.dromara.visor.common.handler.data.strategy.GenericsDataStrategy;
|
||||
import org.dromara.visor.module.asset.handler.host.extra.strategy.HostLabelExtraStrategy;
|
||||
import org.dromara.visor.module.asset.handler.host.extra.strategy.HostRdpExtraStrategy;
|
||||
import org.dromara.visor.module.asset.handler.host.extra.strategy.HostSpecExtraStrategy;
|
||||
import org.dromara.visor.module.asset.handler.host.extra.strategy.HostSshExtraStrategy;
|
||||
import org.dromara.visor.module.asset.handler.host.extra.strategy.*;
|
||||
|
||||
/**
|
||||
* 主机额外配置项策略枚举
|
||||
@@ -58,6 +55,11 @@ public enum HostExtraItemEnum implements GenericsStrategyDefinition {
|
||||
*/
|
||||
RDP(HostRdpExtraStrategy.class, true),
|
||||
|
||||
/**
|
||||
* VNC 额外配置
|
||||
*/
|
||||
VNC(HostVncExtraStrategy.class, true),
|
||||
|
||||
/**
|
||||
* 规格信息配置
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* 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.asset.handler.host.extra.model;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.dromara.visor.common.handler.data.model.GenericsDataModel;
|
||||
|
||||
/**
|
||||
* 主机拓展信息 - vnc 模型
|
||||
*
|
||||
* @author Jiahang Li
|
||||
* @version 1.0.0
|
||||
* @since 2023/12/20 21:36
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class HostVncExtraModel implements GenericsDataModel {
|
||||
|
||||
/**
|
||||
* 端口号
|
||||
*/
|
||||
private Integer port;
|
||||
|
||||
/**
|
||||
* 低带宽模式
|
||||
*/
|
||||
private Boolean lowBandwidthMode;
|
||||
|
||||
/**
|
||||
* 交换红蓝
|
||||
*/
|
||||
private Boolean swapRedBlue;
|
||||
|
||||
}
|
||||
@@ -59,6 +59,7 @@ public class HostRdpExtraStrategy extends AbstractGenericsDataStrategy<HostRdpEx
|
||||
public HostRdpExtraModel getDefault() {
|
||||
return HostRdpExtraModel.builder()
|
||||
.authType(HostExtraAuthTypeEnum.DEFAULT.name())
|
||||
.lowBandwidthMode(false)
|
||||
.build();
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* 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.asset.handler.host.extra.strategy;
|
||||
|
||||
import org.dromara.visor.common.handler.data.strategy.AbstractGenericsDataStrategy;
|
||||
import org.dromara.visor.module.asset.handler.host.extra.model.HostVncExtraModel;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 主机拓展信息 - rdp 模型处理策略
|
||||
*
|
||||
* @author Jiahang Li
|
||||
* @version 1.0.0
|
||||
* @since 2023/12/20 22:17
|
||||
*/
|
||||
@Component
|
||||
public class HostVncExtraStrategy extends AbstractGenericsDataStrategy<HostVncExtraModel> {
|
||||
|
||||
public HostVncExtraStrategy() {
|
||||
super(HostVncExtraModel.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public HostVncExtraModel getDefault() {
|
||||
return HostVncExtraModel.builder()
|
||||
.lowBandwidthMode(false)
|
||||
.swapRedBlue(false)
|
||||
.build();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -24,6 +24,7 @@ package org.dromara.visor.module.asset.service;
|
||||
|
||||
import org.dromara.visor.common.session.config.RdpConnectConfig;
|
||||
import org.dromara.visor.common.session.config.SshConnectConfig;
|
||||
import org.dromara.visor.common.session.config.VncConnectConfig;
|
||||
import org.dromara.visor.module.asset.entity.domain.HostDO;
|
||||
import org.dromara.visor.module.asset.entity.request.host.HostTestConnectRequest;
|
||||
|
||||
@@ -95,4 +96,30 @@ public interface HostConnectService {
|
||||
*/
|
||||
RdpConnectConfig getRdpConnectConfig(HostDO host, Long userId);
|
||||
|
||||
/**
|
||||
* 获取 VNC 连接配置
|
||||
*
|
||||
* @param hostId hostId
|
||||
* @return session
|
||||
*/
|
||||
VncConnectConfig getVncConnectConfig(Long hostId);
|
||||
|
||||
/**
|
||||
* 使用用户配置获取 VNC 连接配置
|
||||
*
|
||||
* @param hostId hostId
|
||||
* @param userId userId
|
||||
* @return session
|
||||
*/
|
||||
VncConnectConfig getVncConnectConfig(Long hostId, Long userId);
|
||||
|
||||
/**
|
||||
* 使用用户配置获取 VNC 连接配置
|
||||
*
|
||||
* @param host host
|
||||
* @param userId userId
|
||||
* @return session
|
||||
*/
|
||||
VncConnectConfig getVncConnectConfig(HostDO host, Long userId);
|
||||
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
*/
|
||||
package org.dromara.visor.module.asset.service.impl;
|
||||
|
||||
import cn.orionsec.kit.lang.utils.Booleans;
|
||||
import cn.orionsec.kit.lang.utils.Exceptions;
|
||||
import cn.orionsec.kit.lang.utils.io.Streams;
|
||||
import cn.orionsec.kit.net.host.SessionStore;
|
||||
@@ -30,6 +31,7 @@ import org.dromara.visor.common.constant.ErrorMessage;
|
||||
import org.dromara.visor.common.session.config.BaseConnectConfig;
|
||||
import org.dromara.visor.common.session.config.RdpConnectConfig;
|
||||
import org.dromara.visor.common.session.config.SshConnectConfig;
|
||||
import org.dromara.visor.common.session.config.VncConnectConfig;
|
||||
import org.dromara.visor.common.session.ssh.SessionStores;
|
||||
import org.dromara.visor.common.utils.Valid;
|
||||
import org.dromara.visor.module.asset.dao.HostDAO;
|
||||
@@ -40,6 +42,7 @@ import org.dromara.visor.module.asset.entity.domain.HostIdentityDO;
|
||||
import org.dromara.visor.module.asset.entity.domain.HostKeyDO;
|
||||
import org.dromara.visor.module.asset.entity.dto.host.HostRdpConfigDTO;
|
||||
import org.dromara.visor.module.asset.entity.dto.host.HostSshConfigDTO;
|
||||
import org.dromara.visor.module.asset.entity.dto.host.HostVncConfigDTO;
|
||||
import org.dromara.visor.module.asset.entity.request.host.HostTestConnectRequest;
|
||||
import org.dromara.visor.module.asset.enums.HostAuthTypeEnum;
|
||||
import org.dromara.visor.module.asset.enums.HostExtraAuthTypeEnum;
|
||||
@@ -48,6 +51,7 @@ import org.dromara.visor.module.asset.enums.HostTypeEnum;
|
||||
import org.dromara.visor.module.asset.handler.host.extra.HostExtraItemEnum;
|
||||
import org.dromara.visor.module.asset.handler.host.extra.model.HostRdpExtraModel;
|
||||
import org.dromara.visor.module.asset.handler.host.extra.model.HostSshExtraModel;
|
||||
import org.dromara.visor.module.asset.handler.host.extra.model.HostVncExtraModel;
|
||||
import org.dromara.visor.module.asset.service.AssetAuthorizedDataService;
|
||||
import org.dromara.visor.module.asset.service.HostConfigService;
|
||||
import org.dromara.visor.module.asset.service.HostConnectService;
|
||||
@@ -108,7 +112,6 @@ public class HostConnectServiceImpl implements HostConnectService {
|
||||
Streams.close(sessionStore);
|
||||
}
|
||||
}
|
||||
// TODO: 其他连接方式
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -189,6 +192,41 @@ public class HostConnectServiceImpl implements HostConnectService {
|
||||
return this.getRdpConnectConfig(host, config, extra);
|
||||
}
|
||||
|
||||
@Override
|
||||
public VncConnectConfig getVncConnectConfig(Long hostId) {
|
||||
log.info("HostConnectService.getVncConnectConfig-withHost hostId: {}", hostId);
|
||||
// 查询主机
|
||||
HostDO host = hostDAO.selectById(hostId);
|
||||
// 查询主机配置
|
||||
HostVncConfigDTO config = hostConfigService.getHostConfig(hostId, HostTypeEnum.VNC.name());
|
||||
// 获取配置
|
||||
return this.getVncConnectConfig(host, config, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public VncConnectConfig getVncConnectConfig(Long hostId, Long userId) {
|
||||
// 查询主机
|
||||
HostDO host = hostDAO.selectById(hostId);
|
||||
Valid.notNull(host, ErrorMessage.HOST_ABSENT);
|
||||
// 获取配置
|
||||
return this.getVncConnectConfig(host, userId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public VncConnectConfig getVncConnectConfig(HostDO host, Long userId) {
|
||||
Long hostId = host.getId();
|
||||
log.info("HostConnectService.getVncConnectConfig hostId: {}, userId: {}", hostId, userId);
|
||||
// 验证权限
|
||||
this.validHostAuthorized(userId, hostId);
|
||||
// 获取主机配置
|
||||
HostVncConfigDTO config = hostConfigService.getHostConfig(hostId, HostTypeEnum.VNC.name());
|
||||
Valid.notNull(config, ErrorMessage.CONFIG_ABSENT);
|
||||
// 查询主机额外配置
|
||||
HostVncExtraModel extra = hostExtraService.getHostExtra(userId, hostId, HostExtraItemEnum.VNC);
|
||||
// 获取连接配置
|
||||
return this.getVncConnectConfig(host, config, extra);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取主机 SSH 连接配置
|
||||
*
|
||||
@@ -254,14 +292,7 @@ public class HostConnectServiceImpl implements HostConnectService {
|
||||
connectConfig.setPassword(config.getPassword());
|
||||
} else if (HostAuthTypeEnum.KEY.equals(authType)) {
|
||||
// 密钥认证
|
||||
Long keyId = config.getKeyId();
|
||||
Valid.notNull(keyId, ErrorMessage.KEY_ABSENT);
|
||||
HostKeyDO key = hostKeyDAO.selectById(keyId);
|
||||
Valid.notNull(key, ErrorMessage.KEY_ABSENT);
|
||||
connectConfig.setKeyId(keyId);
|
||||
connectConfig.setPublicKey(key.getPublicKey());
|
||||
connectConfig.setPrivateKey(key.getPrivateKey());
|
||||
connectConfig.setPrivateKeyPassword(key.getPassword());
|
||||
this.setSshKey(config.getKeyId(), connectConfig);
|
||||
}
|
||||
return connectConfig;
|
||||
}
|
||||
@@ -279,6 +310,8 @@ public class HostConnectServiceImpl implements HostConnectService {
|
||||
// 填充认证信息
|
||||
RdpConnectConfig connectConfig = RdpConnectConfig.builder()
|
||||
.hostPort(config.getPort())
|
||||
.username(config.getUsername())
|
||||
.password(config.getPassword())
|
||||
.versionGt81(config.getVersionGt81())
|
||||
.timezone(config.getTimezone())
|
||||
.keyboardLayout(config.getKeyboardLayout())
|
||||
@@ -304,25 +337,54 @@ public class HostConnectServiceImpl implements HostConnectService {
|
||||
config.setIdentityId(extra.getIdentityId());
|
||||
}
|
||||
}
|
||||
|
||||
// 身份认证
|
||||
HostAuthTypeEnum authType = HostAuthTypeEnum.of(config.getAuthType());
|
||||
if (HostAuthTypeEnum.IDENTITY.equals(authType)) {
|
||||
// 身份认证 - 仅密码
|
||||
authType = HostAuthTypeEnum.PASSWORD;
|
||||
Valid.notNull(config.getIdentityId(), ErrorMessage.IDENTITY_ABSENT);
|
||||
HostIdentityDO identity = hostIdentityDAO.selectById(config.getIdentityId());
|
||||
Valid.notNull(identity, ErrorMessage.IDENTITY_ABSENT);
|
||||
// 设置身份信息
|
||||
config.setUsername(identity.getUsername());
|
||||
config.setPassword(identity.getPassword());
|
||||
// 填充身份认证信息
|
||||
if (HostAuthTypeEnum.IDENTITY.name().equals(config.getAuthType())) {
|
||||
this.setIdentityPasswordAuthorization(config.getIdentityId(), connectConfig);
|
||||
}
|
||||
return connectConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 VNC 连接信息
|
||||
*
|
||||
* @param host host
|
||||
* @param config config
|
||||
* @return info
|
||||
*/
|
||||
private VncConnectConfig getVncConnectConfig(HostDO host,
|
||||
HostVncConfigDTO config,
|
||||
HostVncExtraModel extra) {
|
||||
// 填充认证信息
|
||||
connectConfig.setUsername(config.getUsername());
|
||||
if (HostAuthTypeEnum.PASSWORD.equals(authType)) {
|
||||
// 密码认证
|
||||
connectConfig.setPassword(config.getPassword());
|
||||
VncConnectConfig connectConfig = VncConnectConfig.builder()
|
||||
.hostPort(config.getPort())
|
||||
.username(config.getUsername())
|
||||
.password(config.getPassword())
|
||||
.timezone(config.getTimezone())
|
||||
.clipboardEncoding(config.getClipboardEncoding())
|
||||
.build();
|
||||
// 填充基础主机信息
|
||||
this.setBaseConnectConfig(connectConfig, host);
|
||||
if (extra != null) {
|
||||
// 设置额外配置信息
|
||||
connectConfig.setLowBandwidthMode(extra.getLowBandwidthMode());
|
||||
connectConfig.setSwapRedBlue(extra.getSwapRedBlue());
|
||||
// 设置自定义端口
|
||||
Integer extraPort = extra.getPort();
|
||||
if (extraPort != null) {
|
||||
connectConfig.setHostPort(extraPort);
|
||||
}
|
||||
}
|
||||
// 填充身份认证信息
|
||||
if (HostAuthTypeEnum.IDENTITY.name().equals(config.getAuthType())) {
|
||||
this.setIdentityPasswordAuthorization(config.getIdentityId(), connectConfig);
|
||||
}
|
||||
// 无用户名
|
||||
if (Booleans.isTrue(config.getNoUsername())) {
|
||||
connectConfig.setUsername(null);
|
||||
}
|
||||
// 无密码
|
||||
if (Booleans.isTrue(config.getNoPassword())) {
|
||||
connectConfig.setPassword(null);
|
||||
}
|
||||
return connectConfig;
|
||||
}
|
||||
@@ -366,6 +428,41 @@ public class HostConnectServiceImpl implements HostConnectService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置密码认证
|
||||
*
|
||||
* @param identityId identityId
|
||||
* @param connectConfig connectConfig
|
||||
*/
|
||||
private void setIdentityPasswordAuthorization(Long identityId, BaseConnectConfig connectConfig) {
|
||||
if (identityId == null) {
|
||||
return;
|
||||
}
|
||||
// 查询身份信息
|
||||
HostIdentityDO identity = hostIdentityDAO.selectById(identityId);
|
||||
Valid.notNull(identity, ErrorMessage.IDENTITY_ABSENT);
|
||||
// 设置身份信息
|
||||
connectConfig.setUsername(identity.getUsername());
|
||||
connectConfig.setPassword(identity.getPassword());
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置 SSH 密钥信息
|
||||
*
|
||||
* @param keyId keyId
|
||||
* @param connectConfig connectConfig
|
||||
*/
|
||||
private void setSshKey(Long keyId, SshConnectConfig connectConfig) {
|
||||
Valid.notNull(keyId, ErrorMessage.KEY_ABSENT);
|
||||
// 查询密钥信息
|
||||
HostKeyDO key = hostKeyDAO.selectById(keyId);
|
||||
Valid.notNull(key, ErrorMessage.KEY_ABSENT);
|
||||
connectConfig.setKeyId(keyId);
|
||||
connectConfig.setPublicKey(key.getPublicKey());
|
||||
connectConfig.setPrivateKey(key.getPrivateKey());
|
||||
connectConfig.setPrivateKeyPassword(key.getPassword());
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置基础主机信息
|
||||
*
|
||||
|
||||
@@ -22,11 +22,12 @@
|
||||
*/
|
||||
package org.dromara.visor.module.infra.controller;
|
||||
|
||||
import cn.orionsec.kit.web.servlet.web.Servlets;
|
||||
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.common.constant.CustomHeaderConst;
|
||||
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;
|
||||
@@ -71,10 +72,8 @@ public class UserAggregateController {
|
||||
@GetMapping("/user")
|
||||
@Operation(summary = "获取用户权限聚合信息")
|
||||
public UserAggregateVO getUserAggregateInfo(HttpServletResponse response) {
|
||||
// FIXME KIT
|
||||
// 设置版本号请求头
|
||||
response.setHeader(HttpHeaderConst.ACCESS_CONTROL_EXPOSE_HEADERS, HttpHeaderConst.APP_VERSION);
|
||||
response.setHeader(HttpHeaderConst.APP_VERSION, AppConst.VERSION);
|
||||
Servlets.addCustomHeader(response, CustomHeaderConst.APP_VERSION, AppConst.VERSION);
|
||||
// 获取用户信息
|
||||
return userAggregateService.getUserAggregateInfo();
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ import java.util.concurrent.TimeUnit;
|
||||
public interface PreferenceCacheKeyDefine {
|
||||
|
||||
CacheKeyDefine PREFERENCE = new CacheKeyBuilder()
|
||||
.key("v1:user:prefer:{}:{}")
|
||||
.key("user:prefer:{}:{}")
|
||||
.desc("用户偏好 ${userId} ${type}")
|
||||
.type(JSONObject.class)
|
||||
.struct(RedisCacheStruct.STRING)
|
||||
|
||||
@@ -95,6 +95,16 @@ public class TerminalPreferenceModel implements GenericsDataModel {
|
||||
*/
|
||||
private JSONObject rdpActionBarSetting;
|
||||
|
||||
/**
|
||||
* vnc 图形化设置
|
||||
*/
|
||||
private JSONObject vncGraphSetting;
|
||||
|
||||
/**
|
||||
* vnc 操作栏设置
|
||||
*/
|
||||
private JSONObject vncActionBarSetting;
|
||||
|
||||
/**
|
||||
* 快捷键设置
|
||||
*/
|
||||
@@ -297,6 +307,11 @@ public class TerminalPreferenceModel implements GenericsDataModel {
|
||||
*/
|
||||
private Integer scrollBackLine;
|
||||
|
||||
/**
|
||||
* 替换退格符
|
||||
*/
|
||||
private Boolean replaceBackspace;
|
||||
|
||||
}
|
||||
|
||||
@Data
|
||||
@@ -421,6 +436,11 @@ public class TerminalPreferenceModel implements GenericsDataModel {
|
||||
*/
|
||||
private String position;
|
||||
|
||||
/**
|
||||
* 会话信息
|
||||
*/
|
||||
private Boolean info;
|
||||
|
||||
/**
|
||||
* 显示设置
|
||||
*/
|
||||
@@ -431,18 +451,33 @@ public class TerminalPreferenceModel implements GenericsDataModel {
|
||||
*/
|
||||
private Boolean combinationKey;
|
||||
|
||||
/**
|
||||
* 长按键
|
||||
*/
|
||||
private Boolean triggerKey;
|
||||
|
||||
/**
|
||||
* 剪切板
|
||||
*/
|
||||
private Boolean clipboard;
|
||||
|
||||
/**
|
||||
* 上传
|
||||
* RDP 上传
|
||||
*/
|
||||
private Boolean upload;
|
||||
private Boolean rdpUpload;
|
||||
|
||||
/**
|
||||
* 保存为 rdp 文件
|
||||
* SFTP 上传
|
||||
*/
|
||||
private Boolean sftpUpload;
|
||||
|
||||
/**
|
||||
* 打开 SFTP
|
||||
*/
|
||||
private Boolean openSftp;
|
||||
|
||||
/**
|
||||
* 保存为 RDP 文件
|
||||
*/
|
||||
private Boolean saveRdp;
|
||||
|
||||
@@ -451,6 +486,11 @@ public class TerminalPreferenceModel implements GenericsDataModel {
|
||||
*/
|
||||
private Boolean disconnect;
|
||||
|
||||
/**
|
||||
* 重新连接
|
||||
*/
|
||||
private Boolean reconnect;
|
||||
|
||||
/**
|
||||
* 关闭
|
||||
*/
|
||||
@@ -481,6 +521,118 @@ public class TerminalPreferenceModel implements GenericsDataModel {
|
||||
|
||||
}
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class VncGraphSettingModel implements IJsonObject {
|
||||
|
||||
/**
|
||||
* 显示大小
|
||||
*/
|
||||
private String displaySize;
|
||||
|
||||
/**
|
||||
* 显示宽度
|
||||
*/
|
||||
private Integer displayWidth;
|
||||
|
||||
/**
|
||||
* 显示高度
|
||||
*/
|
||||
private Integer displayHeight;
|
||||
|
||||
/**
|
||||
* 颜色深度
|
||||
*/
|
||||
private Integer colorDepth;
|
||||
|
||||
/**
|
||||
* 无损压缩
|
||||
*/
|
||||
private Boolean forceLossless;
|
||||
|
||||
/**
|
||||
* 光标
|
||||
*/
|
||||
private String cursor;
|
||||
|
||||
/**
|
||||
* 质量等级
|
||||
*/
|
||||
private Integer compressLevel;
|
||||
|
||||
/**
|
||||
* 压缩等级
|
||||
*/
|
||||
private Integer qualityLevel;
|
||||
|
||||
}
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class VncActionBarSettingModel implements IJsonObject {
|
||||
|
||||
/**
|
||||
* 位置
|
||||
*/
|
||||
private String position;
|
||||
|
||||
/**
|
||||
* 会话信息
|
||||
*/
|
||||
private Boolean info;
|
||||
|
||||
/**
|
||||
* 显示设置
|
||||
*/
|
||||
private Boolean display;
|
||||
|
||||
/**
|
||||
* 组合键
|
||||
*/
|
||||
private Boolean combinationKey;
|
||||
|
||||
/**
|
||||
* 长按键
|
||||
*/
|
||||
private Boolean triggerKey;
|
||||
|
||||
/**
|
||||
* 剪切板
|
||||
*/
|
||||
private Boolean clipboard;
|
||||
|
||||
/**
|
||||
* SFTP 上传
|
||||
*/
|
||||
private Boolean sftpUpload;
|
||||
|
||||
/**
|
||||
* 打开 SFTP
|
||||
*/
|
||||
private Boolean openSftp;
|
||||
|
||||
/**
|
||||
* 断开连接
|
||||
*/
|
||||
private Boolean disconnect;
|
||||
|
||||
/**
|
||||
* 重新连接
|
||||
*/
|
||||
private Boolean reconnect;
|
||||
|
||||
|
||||
/**
|
||||
* 关闭
|
||||
*/
|
||||
private Boolean close;
|
||||
|
||||
}
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
|
||||
@@ -68,6 +68,10 @@ public class TerminalPreferenceStrategy extends AbstractGenericsDataStrategy<Ter
|
||||
.rdpActionBarSetting(JSONObject.parseObject(this.getDefaultRdpActionBarSetting()))
|
||||
// rdp 会话设置
|
||||
.rdpSessionSetting(JSONObject.parseObject(this.getDefaultRdpSessionSetting()))
|
||||
// vnc 图形化设置
|
||||
.vncGraphSetting(JSONObject.parseObject(this.getDefaultVncGraphSetting()))
|
||||
// vnc 图形化设置
|
||||
.vncActionBarSetting(JSONObject.parseObject(this.getDefaultVncSessionSetting()))
|
||||
// 快捷键设置
|
||||
.shortcutSetting(JSONObject.parseObject(this.getDefaultShortcutSetting()))
|
||||
.build();
|
||||
@@ -148,6 +152,7 @@ public class TerminalPreferenceStrategy extends AbstractGenericsDataStrategy<Ter
|
||||
.wordSeparator("/\\()\"'` -.,:;<>~!@#$%^&*|+=[]{}~?│")
|
||||
.terminalEmulationType(TerminalType.XTERM.getType())
|
||||
.scrollBackLine(1000)
|
||||
.replaceBackspace(false)
|
||||
.build()
|
||||
.toJsonString();
|
||||
}
|
||||
@@ -201,12 +206,17 @@ public class TerminalPreferenceStrategy extends AbstractGenericsDataStrategy<Ter
|
||||
private String getDefaultRdpActionBarSetting() {
|
||||
return TerminalPreferenceModel.RdpActionBarSettingModel.builder()
|
||||
.position("top")
|
||||
.info(true)
|
||||
.display(true)
|
||||
.combinationKey(true)
|
||||
.triggerKey(false)
|
||||
.clipboard(true)
|
||||
.upload(true)
|
||||
.saveRdp(true)
|
||||
.rdpUpload(true)
|
||||
.sftpUpload(false)
|
||||
.openSftp(false)
|
||||
.saveRdp(false)
|
||||
.disconnect(true)
|
||||
.reconnect(false)
|
||||
.close(true)
|
||||
.build()
|
||||
.toJsonString();
|
||||
@@ -226,6 +236,47 @@ public class TerminalPreferenceStrategy extends AbstractGenericsDataStrategy<Ter
|
||||
.toJsonString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 vnc 图形化默认设置
|
||||
*
|
||||
* @return setting
|
||||
*/
|
||||
private String getDefaultVncGraphSetting() {
|
||||
return TerminalPreferenceModel.VncGraphSettingModel.builder()
|
||||
.displaySize("fit")
|
||||
.displayWidth(0)
|
||||
.displayHeight(0)
|
||||
.colorDepth(24)
|
||||
.forceLossless(true)
|
||||
.cursor("local")
|
||||
.compressLevel(5)
|
||||
.qualityLevel(5)
|
||||
.build()
|
||||
.toJsonString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 vnc 工具栏默认设置
|
||||
*
|
||||
* @return setting
|
||||
*/
|
||||
private String getDefaultVncSessionSetting() {
|
||||
return TerminalPreferenceModel.VncActionBarSettingModel.builder()
|
||||
.position("top")
|
||||
.info(true)
|
||||
.display(true)
|
||||
.combinationKey(true)
|
||||
.triggerKey(false)
|
||||
.clipboard(true)
|
||||
.sftpUpload(true)
|
||||
.openSftp(true)
|
||||
.disconnect(true)
|
||||
.reconnect(false)
|
||||
.close(true)
|
||||
.build()
|
||||
.toJsonString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取默认快捷键设置
|
||||
*
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
)
|
||||
WHERE deleted = 0
|
||||
AND type = 'HOST'
|
||||
AND item IN ('SSH', 'RDP')
|
||||
AND item IN ('SSH', 'RDP', 'VNC')
|
||||
<foreach collection="identityIdList" item="item" separator="OR" open="AND (" close=")">
|
||||
JSON_CONTAINS(value, JSON_OBJECT('identityId', #{item}))
|
||||
</foreach>
|
||||
|
||||
@@ -25,6 +25,7 @@ package org.dromara.visor.module.terminal.configuration;
|
||||
import org.dromara.visor.module.terminal.handler.terminal.TerminalAccessRdpHandler;
|
||||
import org.dromara.visor.module.terminal.handler.terminal.TerminalAccessSftpHandler;
|
||||
import org.dromara.visor.module.terminal.handler.terminal.TerminalAccessSshHandler;
|
||||
import org.dromara.visor.module.terminal.handler.terminal.TerminalAccessVncHandler;
|
||||
import org.dromara.visor.module.terminal.handler.transfer.TransferMessageDispatcher;
|
||||
import org.dromara.visor.module.terminal.interceptor.TerminalAccessInterceptor;
|
||||
import org.dromara.visor.module.terminal.interceptor.TerminalTransferInterceptor;
|
||||
@@ -63,6 +64,9 @@ public class TerminalWebSocketConfiguration implements WebSocketConfigurer {
|
||||
@Resource
|
||||
private TerminalAccessRdpHandler terminalAccessRdpHandler;
|
||||
|
||||
@Resource
|
||||
private TerminalAccessVncHandler terminalAccessVncHandler;
|
||||
|
||||
@Resource
|
||||
private TransferMessageDispatcher transferMessageDispatcher;
|
||||
|
||||
@@ -80,6 +84,10 @@ public class TerminalWebSocketConfiguration implements WebSocketConfigurer {
|
||||
registry.addHandler(terminalAccessRdpHandler, prefix + "/terminal/access/rdp/{accessToken}")
|
||||
.addInterceptors(terminalAccessInterceptor)
|
||||
.setAllowedOrigins("*");
|
||||
// VNC 终端会话
|
||||
registry.addHandler(terminalAccessVncHandler, prefix + "/terminal/access/vnc/{accessToken}")
|
||||
.addInterceptors(terminalAccessInterceptor)
|
||||
.setAllowedOrigins("*");
|
||||
// 文件传输
|
||||
registry.addHandler(transferMessageDispatcher, prefix + "/terminal/transfer/{transferToken}")
|
||||
.addInterceptors(terminalTransferInterceptor)
|
||||
|
||||
@@ -24,9 +24,11 @@ package org.dromara.visor.module.terminal.convert;
|
||||
|
||||
import org.dromara.visor.common.session.config.RdpConnectConfig;
|
||||
import org.dromara.visor.common.session.config.SshConnectConfig;
|
||||
import org.dromara.visor.common.session.config.VncConnectConfig;
|
||||
import org.dromara.visor.module.terminal.handler.terminal.model.config.TerminalSessionRdpConfig;
|
||||
import org.dromara.visor.module.terminal.handler.terminal.model.config.TerminalSessionSftpConfig;
|
||||
import org.dromara.visor.module.terminal.handler.terminal.model.config.TerminalSessionSshConfig;
|
||||
import org.dromara.visor.module.terminal.handler.terminal.model.config.TerminalSessionVncConfig;
|
||||
import org.mapstruct.Mapper;
|
||||
import org.mapstruct.factory.Mappers;
|
||||
|
||||
@@ -48,4 +50,6 @@ public interface TerminalSessionConvert {
|
||||
|
||||
TerminalSessionRdpConfig toRdp(RdpConnectConfig domain);
|
||||
|
||||
TerminalSessionVncConfig toVnc(VncConnectConfig domain);
|
||||
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ public enum TerminalConnectTypeEnum {
|
||||
/**
|
||||
* vnc
|
||||
*/
|
||||
// VNC,
|
||||
VNC,
|
||||
|
||||
;
|
||||
|
||||
|
||||
@@ -89,7 +89,6 @@ public class GuacdTunnel implements IGuacdTunnel {
|
||||
@Override
|
||||
public void connect() throws GuacdException {
|
||||
try {
|
||||
// TODO 端口转发
|
||||
this.socket = new ConfiguredGuacamoleSocket(new InetGuacamoleSocket(serverAddress, serverPort), serverConfig, clientConfig);
|
||||
this.tunnel = new CustomGuacamoleTunnel(uuid, socket);
|
||||
} catch (GuacamoleException e) {
|
||||
@@ -192,12 +191,16 @@ public class GuacdTunnel implements IGuacdTunnel {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void size(int width, int height, int dpi) {
|
||||
public void size(int width, int height) {
|
||||
clientConfig.setOptimalScreenWidth(width);
|
||||
clientConfig.setOptimalScreenHeight(height);
|
||||
clientConfig.setOptimalResolution(dpi);
|
||||
this.setParameter(GuacdConst.WIDTH, width);
|
||||
this.setParameter(GuacdConst.HEIGHT, height);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dpi(int dpi) {
|
||||
clientConfig.setOptimalResolution(dpi);
|
||||
this.setParameter(GuacdConst.DPI, dpi);
|
||||
}
|
||||
|
||||
|
||||
@@ -64,9 +64,15 @@ public interface IGuacdTunnel extends Runnable, Executable, SafeCloseable {
|
||||
*
|
||||
* @param width width
|
||||
* @param height height
|
||||
* @param dpi dpi
|
||||
*/
|
||||
void size(int width, int height, int dpi);
|
||||
void size(int width, int height);
|
||||
|
||||
/**
|
||||
* dpi
|
||||
*
|
||||
* @param dpi dpi
|
||||
*/
|
||||
void dpi(int dpi);
|
||||
|
||||
/**
|
||||
* 设置时区
|
||||
|
||||
@@ -392,6 +392,16 @@ public interface GuacdConst {
|
||||
*/
|
||||
String CLIPBOARD_ENCODING = "clipboard-encoding";
|
||||
|
||||
/**
|
||||
* 压缩等级
|
||||
*/
|
||||
String COMPRESS_LEVEL = "compress-level";
|
||||
|
||||
/**
|
||||
* 质量等级
|
||||
*/
|
||||
String QUALITY_LEVEL = "quality-level";
|
||||
|
||||
// -------------------- const --------------------
|
||||
|
||||
String RESIZE_METHOD_DISPLAY_UPDATE = "display-update";
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.dromara.visor.module.terminal.handler.terminal.enums.InputProtocolEnum;
|
||||
import org.dromara.visor.module.terminal.handler.terminal.model.TerminalChannelProps;
|
||||
import org.dromara.visor.module.terminal.handler.terminal.sender.IGuacdTerminalSender;
|
||||
import org.dromara.visor.module.terminal.handler.terminal.sender.WebsocketGuacdTerminalSender;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.socket.TextMessage;
|
||||
import org.springframework.web.socket.WebSocketSession;
|
||||
|
||||
/**
|
||||
* VNC 终端处理器
|
||||
*
|
||||
* @author Jiahang Li
|
||||
* @version 1.0.0
|
||||
* @since 2023/12/28 14:33
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class TerminalAccessVncHandler extends AbstractTerminalAccessHandler<IGuacdTerminalSender> {
|
||||
|
||||
@Override
|
||||
protected IGuacdTerminalSender createSender(WebSocketSession channel) {
|
||||
return new WebsocketGuacdTerminalSender(channel);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void handleMessage(WebSocketSession channel, TextMessage message, TerminalChannelProps props, IGuacdTerminalSender sender) {
|
||||
String payload = message.getPayload();
|
||||
try {
|
||||
// 解析类型
|
||||
InputProtocolEnum type = InputProtocolEnum.of(payload);
|
||||
if (type == null) {
|
||||
return;
|
||||
}
|
||||
// 解析并处理消息
|
||||
type.getHandler().handle(props, sender, type.parse(payload));
|
||||
} catch (Exception e) {
|
||||
log.error("TerminalAccessVncHandler-handleMessage-error id: {}, msg: {}", channel.getId(), payload, e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -26,7 +26,7 @@ import lombok.extern.slf4j.Slf4j;
|
||||
import org.dromara.visor.module.terminal.handler.terminal.model.TerminalChannelProps;
|
||||
import org.dromara.visor.module.terminal.handler.terminal.model.request.GuacdInstructionRequest;
|
||||
import org.dromara.visor.module.terminal.handler.terminal.sender.IGuacdTerminalSender;
|
||||
import org.dromara.visor.module.terminal.handler.terminal.session.IRdpSession;
|
||||
import org.dromara.visor.module.terminal.handler.terminal.session.IGuacdSession;
|
||||
import org.dromara.visor.module.terminal.handler.terminal.session.ITerminalSession;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@@ -45,20 +45,10 @@ public class GuacdInstructionHandler extends AbstractTerminalHandler<IGuacdTermi
|
||||
public void handle(TerminalChannelProps props, IGuacdTerminalSender sender, GuacdInstructionRequest payload) {
|
||||
// 获取会话
|
||||
ITerminalSession session = terminalManager.getSession(props.getId());
|
||||
if (session instanceof IRdpSession) {
|
||||
// 处理 rdp 指令
|
||||
this.processRdpInstruction((IRdpSession) session, payload.getInstruction());
|
||||
// 发送指令
|
||||
if (session instanceof IGuacdSession) {
|
||||
((IGuacdSession) session).write(payload.getInstruction());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 rdp 指令
|
||||
*
|
||||
* @param session session
|
||||
* @param instruction instruction
|
||||
*/
|
||||
private void processRdpInstruction(IRdpSession session, String instruction) {
|
||||
session.write(instruction);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ import org.dromara.visor.common.constant.ExtraFieldConst;
|
||||
import org.dromara.visor.common.session.config.BaseConnectConfig;
|
||||
import org.dromara.visor.common.session.config.RdpConnectConfig;
|
||||
import org.dromara.visor.common.session.config.SshConnectConfig;
|
||||
import org.dromara.visor.common.session.config.VncConnectConfig;
|
||||
import org.dromara.visor.common.session.ssh.SessionStores;
|
||||
import org.dromara.visor.framework.biz.operator.log.core.model.OperatorLogModel;
|
||||
import org.dromara.visor.framework.biz.operator.log.core.utils.OperatorLogs;
|
||||
@@ -52,10 +53,7 @@ import org.dromara.visor.module.terminal.handler.terminal.constant.SessionCloseC
|
||||
import org.dromara.visor.module.terminal.handler.terminal.constant.TerminalMessage;
|
||||
import org.dromara.visor.module.terminal.handler.terminal.model.TerminalChannelExtra;
|
||||
import org.dromara.visor.module.terminal.handler.terminal.model.TerminalChannelProps;
|
||||
import org.dromara.visor.module.terminal.handler.terminal.model.config.ITerminalSessionConfig;
|
||||
import org.dromara.visor.module.terminal.handler.terminal.model.config.TerminalSessionRdpConfig;
|
||||
import org.dromara.visor.module.terminal.handler.terminal.model.config.TerminalSessionSftpConfig;
|
||||
import org.dromara.visor.module.terminal.handler.terminal.model.config.TerminalSessionSshConfig;
|
||||
import org.dromara.visor.module.terminal.handler.terminal.model.config.*;
|
||||
import org.dromara.visor.module.terminal.handler.terminal.model.request.TerminalConnectRequest;
|
||||
import org.dromara.visor.module.terminal.handler.terminal.model.transport.TerminalConnectBody;
|
||||
import org.dromara.visor.module.terminal.handler.terminal.model.transport.TerminalSetInfo;
|
||||
@@ -63,10 +61,7 @@ import org.dromara.visor.module.terminal.handler.terminal.sender.IGuacdTerminalS
|
||||
import org.dromara.visor.module.terminal.handler.terminal.sender.ISftpTerminalSender;
|
||||
import org.dromara.visor.module.terminal.handler.terminal.sender.ISshTerminalSender;
|
||||
import org.dromara.visor.module.terminal.handler.terminal.sender.ITerminalSender;
|
||||
import org.dromara.visor.module.terminal.handler.terminal.session.ITerminalSession;
|
||||
import org.dromara.visor.module.terminal.handler.terminal.session.RdpSession;
|
||||
import org.dromara.visor.module.terminal.handler.terminal.session.SftpSession;
|
||||
import org.dromara.visor.module.terminal.handler.terminal.session.SshSession;
|
||||
import org.dromara.visor.module.terminal.handler.terminal.session.*;
|
||||
import org.dromara.visor.module.terminal.service.TerminalConnectLogService;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@@ -238,6 +233,12 @@ public class TerminalConnectHandler extends AbstractTerminalHandler<ITerminalSen
|
||||
config.setDpi(connectParams.getDpi());
|
||||
this.setBaseSessionConfig(config, connectLog, connectParams);
|
||||
session = new RdpSession(props, (IGuacdTerminalSender) sender, config, guacdConfig);
|
||||
} else if (TerminalConnectTypeEnum.VNC.name().equals(connectType)) {
|
||||
// 打开 vnc 会话
|
||||
TerminalSessionVncConfig config = TerminalSessionConvert.MAPPER.toVnc((VncConnectConfig) connectConfig);
|
||||
config.setDpi(connectParams.getDpi());
|
||||
this.setBaseSessionConfig(config, connectLog, connectParams);
|
||||
session = new VncSession(props, (IGuacdTerminalSender) sender, config, guacdConfig);
|
||||
} else {
|
||||
throw Exceptions.unsupported();
|
||||
}
|
||||
|
||||
@@ -26,8 +26,7 @@ import lombok.extern.slf4j.Slf4j;
|
||||
import org.dromara.visor.module.terminal.handler.terminal.model.TerminalChannelProps;
|
||||
import org.dromara.visor.module.terminal.handler.terminal.model.request.TerminalResizeRequest;
|
||||
import org.dromara.visor.module.terminal.handler.terminal.sender.ITerminalSender;
|
||||
import org.dromara.visor.module.terminal.handler.terminal.session.IRdpSession;
|
||||
import org.dromara.visor.module.terminal.handler.terminal.session.ISshSession;
|
||||
import org.dromara.visor.module.terminal.handler.terminal.session.IResizeableSession;
|
||||
import org.dromara.visor.module.terminal.handler.terminal.session.ITerminalSession;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@@ -44,16 +43,10 @@ public class TerminalResizeHandler extends AbstractTerminalHandler<ITerminalSend
|
||||
|
||||
@Override
|
||||
public void handle(TerminalChannelProps props, ITerminalSender sender, TerminalResizeRequest payload) {
|
||||
Integer width = payload.getWidth();
|
||||
Integer height = payload.getHeight();
|
||||
// 获取会话
|
||||
ITerminalSession session = terminalManager.getSession(props.getId());
|
||||
if (session instanceof ISshSession) {
|
||||
// SSH
|
||||
((ISshSession) session).resize(width, height);
|
||||
} else if (session instanceof IRdpSession) {
|
||||
// RDP
|
||||
((IRdpSession) session).resize(width, height);
|
||||
if (session instanceof IResizeableSession) {
|
||||
((IResizeableSession) session).resize(payload.getWidth(), payload.getHeight());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -138,4 +138,19 @@ public class TerminalChannelExtra {
|
||||
|
||||
// -------------------- vnc --------------------
|
||||
|
||||
/**
|
||||
* 光标
|
||||
*/
|
||||
private String cursor;
|
||||
|
||||
/**
|
||||
* 压缩等级
|
||||
*/
|
||||
private Integer compressLevel;
|
||||
|
||||
/**
|
||||
* 质量等级
|
||||
*/
|
||||
private Integer qualityLevel;
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* 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.model.config;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
import org.dromara.visor.common.session.config.VncConnectConfig;
|
||||
|
||||
/**
|
||||
* 终端会话配置 VNC
|
||||
*
|
||||
* @author Jiahang Li
|
||||
* @version 1.0.0
|
||||
* @since 2025/6/24 17:08
|
||||
*/
|
||||
@Data
|
||||
@SuperBuilder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class TerminalSessionVncConfig extends VncConnectConfig implements ITerminalSessionConfig {
|
||||
|
||||
@Schema(description = "logId")
|
||||
private Long logId;
|
||||
|
||||
@Schema(description = "宽")
|
||||
private Integer width;
|
||||
|
||||
@Schema(description = "高")
|
||||
private Integer height;
|
||||
|
||||
@Schema(description = "dpi")
|
||||
private Integer dpi;
|
||||
|
||||
}
|
||||
@@ -23,9 +23,11 @@
|
||||
package org.dromara.visor.module.terminal.handler.terminal.session;
|
||||
|
||||
import cn.orionsec.kit.lang.utils.Exceptions;
|
||||
import org.dromara.visor.common.utils.AesEncryptUtils;
|
||||
import org.dromara.visor.module.terminal.define.TerminalThreadPools;
|
||||
import org.dromara.visor.module.terminal.handler.guacd.IGuacdTunnel;
|
||||
import org.dromara.visor.module.terminal.handler.terminal.constant.TerminalMessage;
|
||||
import org.dromara.visor.module.terminal.handler.terminal.model.TerminalChannelExtra;
|
||||
import org.dromara.visor.module.terminal.handler.terminal.model.TerminalChannelProps;
|
||||
import org.dromara.visor.module.terminal.handler.terminal.model.config.ITerminalSessionConfig;
|
||||
import org.dromara.visor.module.terminal.handler.terminal.sender.IGuacdTerminalSender;
|
||||
@@ -70,10 +72,51 @@ public abstract class AbstractGuacdSession<C extends ITerminalSessionConfig>
|
||||
*/
|
||||
protected abstract IGuacdTunnel createTunnel();
|
||||
|
||||
/**
|
||||
* 是否为低带宽模式
|
||||
*
|
||||
* @return is
|
||||
*/
|
||||
protected abstract boolean isLowBandwidthMode();
|
||||
|
||||
/**
|
||||
* 设置 tunnel 参数
|
||||
*/
|
||||
protected abstract void setTunnelParams();
|
||||
protected void setTunnelParams() {
|
||||
// 设置低带宽模式
|
||||
if (this.isLowBandwidthMode()) {
|
||||
this.setLowBandwidthMode();
|
||||
}
|
||||
// 主机信息
|
||||
tunnel.remote(config.getHostAddress(), config.getHostPort());
|
||||
// 身份信息
|
||||
tunnel.auth(config.getUsername(), AesEncryptUtils.decryptAsString(config.getPassword()));
|
||||
// 大小
|
||||
tunnel.size(config.getWidth(), config.getHeight());
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置低带宽模式
|
||||
*/
|
||||
protected void setLowBandwidthMode() {
|
||||
TerminalChannelExtra extra = props.getExtra();
|
||||
extra.setColorDepth(8);
|
||||
extra.setForceLossless(false);
|
||||
extra.setEnableWallpaper(false);
|
||||
extra.setEnableTheming(false);
|
||||
extra.setEnableFontSmoothing(false);
|
||||
extra.setEnableFullWindowDrag(false);
|
||||
extra.setEnableDesktopComposition(false);
|
||||
extra.setEnableMenuAnimations(false);
|
||||
extra.setDisableBitmapCaching(false);
|
||||
extra.setDisableOffscreenCaching(false);
|
||||
extra.setDisableGlyphCaching(false);
|
||||
extra.setDisableGfx(false);
|
||||
extra.setEnableAudioInput(false);
|
||||
extra.setEnableAudioOutput(false);
|
||||
extra.setCompressLevel(9);
|
||||
extra.setQualityLevel(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行连接
|
||||
@@ -92,6 +135,13 @@ public abstract class AbstractGuacdSession<C extends ITerminalSessionConfig>
|
||||
tunnel.write(data);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void resize(int width, int height) {
|
||||
config.setWidth(width);
|
||||
config.setHeight(height);
|
||||
tunnel.writeInstruction("size", String.valueOf(width), String.valueOf(height));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void keepAlive() {
|
||||
// guacd 有内部实现
|
||||
|
||||
@@ -29,7 +29,7 @@ package org.dromara.visor.module.terminal.handler.terminal.session;
|
||||
* @version 1.0.0
|
||||
* @since 2025/3/30 17:42
|
||||
*/
|
||||
public interface IGuacdSession extends ITerminalSession {
|
||||
public interface IGuacdSession extends ITerminalSession, IResizeableSession {
|
||||
|
||||
/**
|
||||
* 写入数据
|
||||
|
||||
@@ -30,13 +30,4 @@ package org.dromara.visor.module.terminal.handler.terminal.session;
|
||||
* @since 2025/3/30 17:42
|
||||
*/
|
||||
public interface IRdpSession extends IGuacdSession {
|
||||
|
||||
/**
|
||||
* 重置大小
|
||||
*
|
||||
* @param width width
|
||||
* @param height height
|
||||
*/
|
||||
void resize(int width, int height);
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* 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.session;
|
||||
|
||||
/**
|
||||
* 可修改大小的会话
|
||||
*
|
||||
* @author Jiahang Li
|
||||
* @version 1.0.0
|
||||
* @since 2025/7/3 2:16
|
||||
*/
|
||||
public interface IResizeableSession {
|
||||
|
||||
/**
|
||||
* 修改大小
|
||||
*
|
||||
* @param width width
|
||||
* @param height height
|
||||
*/
|
||||
void resize(int width, int height);
|
||||
|
||||
}
|
||||
@@ -32,15 +32,7 @@ import org.dromara.visor.module.terminal.handler.terminal.sender.ISshTerminalSen
|
||||
* @version 1.0.0
|
||||
* @since 2024/2/4 16:47
|
||||
*/
|
||||
public interface ISshSession extends ITerminalSession {
|
||||
|
||||
/**
|
||||
* 重置大小
|
||||
*
|
||||
* @param width width
|
||||
* @param height height
|
||||
*/
|
||||
void resize(int width, int height);
|
||||
public interface ISshSession extends ITerminalSession, IResizeableSession {
|
||||
|
||||
/**
|
||||
* 写入内容
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* 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.session;
|
||||
|
||||
/**
|
||||
* vnc 会话
|
||||
*
|
||||
* @author Jiahang Li
|
||||
* @version 1.0.0
|
||||
* @since 2025/7/3 2:04
|
||||
*/
|
||||
public interface IVncSession extends IGuacdSession {
|
||||
}
|
||||
@@ -27,7 +27,6 @@ import cn.orionsec.kit.lang.utils.Strings;
|
||||
import cn.orionsec.kit.lang.utils.io.Files1;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.dromara.visor.common.constant.AppConst;
|
||||
import org.dromara.visor.common.utils.AesEncryptUtils;
|
||||
import org.dromara.visor.module.common.config.GuacdConfig;
|
||||
import org.dromara.visor.module.terminal.enums.DriveMountModeEnum;
|
||||
import org.dromara.visor.module.terminal.handler.guacd.GuacdTunnel;
|
||||
@@ -66,19 +65,13 @@ public class RdpSession extends AbstractGuacdSession<TerminalSessionRdpConfig> i
|
||||
|
||||
@Override
|
||||
protected void setTunnelParams() {
|
||||
super.setTunnelParams();
|
||||
// 设置额外参数
|
||||
TerminalChannelExtra extra = props.getExtra();
|
||||
// 音频输入会导致无法连接先写死
|
||||
extra.setEnableAudioInput(false);
|
||||
// 设置低带宽模式
|
||||
if (Booleans.isTrue(config.getLowBandwidthMode())) {
|
||||
this.setLowBandwidthMode(extra);
|
||||
}
|
||||
// 主机信息
|
||||
tunnel.remote(config.getHostAddress(), config.getHostPort());
|
||||
// 身份信息
|
||||
tunnel.auth(config.getUsername(), AesEncryptUtils.decryptAsString(config.getPassword()));
|
||||
// 大小
|
||||
tunnel.size(config.getWidth(), config.getHeight(), config.getDpi());
|
||||
// dpi
|
||||
tunnel.dpi(config.getDpi());
|
||||
// 时区
|
||||
tunnel.timezone(config.getTimezone());
|
||||
// 忽略证书
|
||||
@@ -140,32 +133,8 @@ public class RdpSession extends AbstractGuacdSession<TerminalSessionRdpConfig> i
|
||||
}
|
||||
|
||||
@Override
|
||||
public void resize(int width, int height) {
|
||||
config.setWidth(width);
|
||||
config.setHeight(height);
|
||||
tunnel.writeInstruction("size", String.valueOf(width), String.valueOf(height));
|
||||
}
|
||||
|
||||
/**
|
||||
* 低带宽模式
|
||||
*
|
||||
* @param extra extra
|
||||
*/
|
||||
private void setLowBandwidthMode(TerminalChannelExtra extra) {
|
||||
extra.setColorDepth(8);
|
||||
extra.setForceLossless(false);
|
||||
extra.setEnableWallpaper(false);
|
||||
extra.setEnableTheming(false);
|
||||
extra.setEnableFontSmoothing(false);
|
||||
extra.setEnableFullWindowDrag(false);
|
||||
extra.setEnableDesktopComposition(false);
|
||||
extra.setEnableMenuAnimations(false);
|
||||
extra.setDisableBitmapCaching(false);
|
||||
extra.setDisableOffscreenCaching(false);
|
||||
extra.setDisableGlyphCaching(false);
|
||||
extra.setDisableGfx(false);
|
||||
extra.setEnableAudioInput(false);
|
||||
extra.setEnableAudioOutput(false);
|
||||
protected boolean isLowBandwidthMode() {
|
||||
return Booleans.isTrue(config.getLowBandwidthMode());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* 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.session;
|
||||
|
||||
import cn.orionsec.kit.lang.utils.Booleans;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.dromara.visor.module.common.config.GuacdConfig;
|
||||
import org.dromara.visor.module.terminal.handler.guacd.GuacdTunnel;
|
||||
import org.dromara.visor.module.terminal.handler.guacd.IGuacdTunnel;
|
||||
import org.dromara.visor.module.terminal.handler.guacd.constant.GuacdConst;
|
||||
import org.dromara.visor.module.terminal.handler.guacd.constant.GuacdProtocol;
|
||||
import org.dromara.visor.module.terminal.handler.terminal.model.TerminalChannelExtra;
|
||||
import org.dromara.visor.module.terminal.handler.terminal.model.TerminalChannelProps;
|
||||
import org.dromara.visor.module.terminal.handler.terminal.model.config.TerminalSessionVncConfig;
|
||||
import org.dromara.visor.module.terminal.handler.terminal.sender.IGuacdTerminalSender;
|
||||
|
||||
/**
|
||||
* vnc 会话
|
||||
*
|
||||
* @author Jiahang Li
|
||||
* @version 1.0.0
|
||||
* @since 2025/3/30 17:44
|
||||
*/
|
||||
@Slf4j
|
||||
public class VncSession extends AbstractGuacdSession<TerminalSessionVncConfig> implements IVncSession {
|
||||
|
||||
private final GuacdConfig guacdConfig;
|
||||
|
||||
public VncSession(TerminalChannelProps props,
|
||||
IGuacdTerminalSender sender,
|
||||
TerminalSessionVncConfig config,
|
||||
GuacdConfig guacdConfig) {
|
||||
super(props, sender, config);
|
||||
this.guacdConfig = guacdConfig;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected IGuacdTunnel createTunnel() {
|
||||
return new GuacdTunnel(GuacdProtocol.VNC, sessionId, guacdConfig.getHost(), guacdConfig.getPort());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setTunnelParams() {
|
||||
super.setTunnelParams();
|
||||
// 设置额外参数
|
||||
TerminalChannelExtra extra = props.getExtra();
|
||||
// 时区
|
||||
tunnel.timezone(config.getTimezone());
|
||||
// 显示设置
|
||||
tunnel.setParameter(GuacdConst.COLOR_DEPTH, extra.getColorDepth());
|
||||
tunnel.setParameter(GuacdConst.FORCE_LOSSLESS, extra.getForceLossless());
|
||||
tunnel.setParameter(GuacdConst.COMPRESS_LEVEL, extra.getCompressLevel());
|
||||
tunnel.setParameter(GuacdConst.QUALITY_LEVEL, extra.getQualityLevel());
|
||||
// 交换红蓝
|
||||
tunnel.setParameter(GuacdConst.SWAP_RED_BLUE, config.getSwapRedBlue());
|
||||
// 光标设置
|
||||
tunnel.setParameter(GuacdConst.CURSOR, extra.getCursor());
|
||||
// 编码设置
|
||||
tunnel.setParameter(GuacdConst.CLIPBOARD_ENCODING, config.getClipboardEncoding());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isLowBandwidthMode() {
|
||||
return Booleans.isTrue(config.getLowBandwidthMode());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -79,7 +79,7 @@ public class SftpFileUtils {
|
||||
SftpFileVO vo = new SftpFileVO();
|
||||
vo.setName(file.getName());
|
||||
vo.setPath(file.getPath());
|
||||
vo.setSuffix(Files1.getSuffix(file.getName()));
|
||||
vo.setSuffix(Files1.getFileNameSuffix(file.getName()));
|
||||
vo.setSize(file.getSize());
|
||||
vo.setPermission(file.getPermission());
|
||||
vo.setUid(file.getUid());
|
||||
|
||||
@@ -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.1
|
||||
VITE_APP_VERSION=2.4.2
|
||||
|
||||
@@ -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.1
|
||||
VITE_APP_VERSION=2.4.2
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "orion-visor-ui",
|
||||
"description": "Orion Visor UI",
|
||||
"version": "2.4.1",
|
||||
"version": "2.4.2",
|
||||
"private": true,
|
||||
"author": "Jiahang Li",
|
||||
"license": "Apache 2.0",
|
||||
|
||||
@@ -51,6 +51,16 @@ export interface HostRdpConfig extends HostBaseConfig {
|
||||
remoteAppArgs?: string;
|
||||
}
|
||||
|
||||
// 主机 VNC 配置
|
||||
export interface HostVncConfig extends HostBaseConfig {
|
||||
identityId?: number;
|
||||
noUsername?: boolean;
|
||||
noPassword?: boolean;
|
||||
portForwardId?: number;
|
||||
timezone?: string;
|
||||
clipboardEncoding?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新主机配置
|
||||
*/
|
||||
|
||||
@@ -33,6 +33,13 @@ export interface HostRdpExtraSettingModel {
|
||||
initialProgram: string;
|
||||
}
|
||||
|
||||
// VNC 额外配置
|
||||
export interface HostVncExtraSettingModel {
|
||||
port: number;
|
||||
lowBandwidthMode: boolean;
|
||||
swapRedBlue: boolean;
|
||||
}
|
||||
|
||||
// 标签额外配置
|
||||
export interface HostLabelExtraSettingModel {
|
||||
alias: string;
|
||||
|
||||
@@ -5,7 +5,7 @@ import axios from 'axios';
|
||||
import qs from 'query-string';
|
||||
|
||||
// 主机类型
|
||||
export type HostType = 'SSH' | string | undefined;
|
||||
export type HostType = 'SSH' | 'RDP' | 'VNC' | string | undefined;
|
||||
|
||||
/**
|
||||
* 主机创建请求
|
||||
|
||||
@@ -619,5 +619,189 @@ body[terminal-theme='dark'] .arco-modal-container {
|
||||
font-size: 16px;
|
||||
margin: 0 8px 0 4px;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// guacd 容器
|
||||
.guacd-container {
|
||||
|
||||
// guacd 视口
|
||||
.guacd-viewport {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
:deep(> div) {
|
||||
position: relative;
|
||||
z-index: 8;
|
||||
}
|
||||
}
|
||||
|
||||
// guacd 状态遮罩
|
||||
.guacd-status-mask {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
backdrop-filter: blur(10px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
// guacd 工具栏
|
||||
.guacd-action-bar {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
z-index: 9998;
|
||||
|
||||
&.top {
|
||||
top: 4px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
&.right {
|
||||
right: 4px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
// 工具栏触发器
|
||||
.action-bar-trigger {
|
||||
display: flex;
|
||||
border-radius: 8px;
|
||||
transition: .3s all;
|
||||
background: var(--color-bg-rdp-toolbar);
|
||||
filter: contrast(50%) brightness(50%);
|
||||
|
||||
&.top {
|
||||
width: 240px;
|
||||
height: 8px;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(2px);
|
||||
}
|
||||
}
|
||||
|
||||
&.right {
|
||||
width: 8px;
|
||||
height: 228px;
|
||||
|
||||
&:hover {
|
||||
transform: translateX(-2px);
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: var(--color-bg-rdp-toolbar-hover);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// guacd 工具栏
|
||||
@guacd-action-size: 42px;
|
||||
|
||||
.guacd-action-bar-popover {
|
||||
--actions-width: calc(var(--action-count) * @guacd-action-size + (var(--action-count) - 1) * 16px);
|
||||
background: var(--color-bg-2);
|
||||
|
||||
.arco-popover-content {
|
||||
margin-top: 0;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.action-bar-button {
|
||||
width: @guacd-action-size !important;
|
||||
height: @guacd-action-size !important;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.action-bar-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.action-bar-content-footer {
|
||||
margin-top: 12px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.display-size-label {
|
||||
padding-right: 6px;
|
||||
user-select: none;
|
||||
text-align: end;
|
||||
}
|
||||
|
||||
.display-size-input {
|
||||
width: 198px;
|
||||
}
|
||||
|
||||
.action-bar-upload, .action-bar-clipboard {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.combination-key-item {
|
||||
span {
|
||||
display: block;
|
||||
padding: 6px 12px;
|
||||
cursor: pointer;
|
||||
background: var(--color-fill-1);
|
||||
border-radius: 2px;
|
||||
user-select: none;
|
||||
transition: 0.2s ALL;
|
||||
|
||||
&:hover {
|
||||
background: var(--color-fill-2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.guacd-action-bar-popover.top {
|
||||
.arco-popover-content {
|
||||
flex-direction: column;
|
||||
width: var(--actions-width);
|
||||
}
|
||||
|
||||
.action-bar-content {
|
||||
margin-top: 16px;
|
||||
max-height: 224px;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.action-bar-upload, .action-bar-clipboard {
|
||||
height: 186px;
|
||||
}
|
||||
}
|
||||
|
||||
.guacd-action-bar-popover.right {
|
||||
.arco-popover-content {
|
||||
flex-direction: row-reverse;
|
||||
height: var(--actions-width);
|
||||
}
|
||||
|
||||
.action-bar-content {
|
||||
margin-right: 16px;
|
||||
width: 344px;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.action-bar-upload, .action-bar-clipboard {
|
||||
height: calc(var(--actions-width) - 40px);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
:bordered="false"
|
||||
:hoverable="true"
|
||||
:body-style="cardBodyStyle as Record<string, any>"
|
||||
@contextmenu.prevent="() => false"
|
||||
@click="bubblesEmitter(CardEmitter.CLICK, item, index)"
|
||||
@dblclick="bubblesEmitter(CardEmitter.DBL_CLICK, item, index)">
|
||||
<!-- 标题 -->
|
||||
|
||||
@@ -33,7 +33,9 @@
|
||||
v-bind="cardLayoutCols"
|
||||
:class="{ 'disabled-col': item.disabled === true }">
|
||||
<!-- 右键菜单 -->
|
||||
<a-dropdown trigger="contextMenu" alignPoint>
|
||||
<a-dropdown trigger="contextMenu"
|
||||
:disabled="!$slots.contextMenu"
|
||||
alignPoint>
|
||||
<!-- 卡片 -->
|
||||
<card-item v-bind="props"
|
||||
:index="index"
|
||||
@@ -48,7 +50,7 @@
|
||||
</template>
|
||||
</card-item>
|
||||
<!-- 右键菜单 -->
|
||||
<template v-if="contextMenu" #content>
|
||||
<template v-if="$slots.contextMenu" #content>
|
||||
<slot name="contextMenu"
|
||||
:record="item"
|
||||
:index="index"
|
||||
@@ -92,7 +94,6 @@
|
||||
pagination: false,
|
||||
loading: false,
|
||||
cardHeight: '100%',
|
||||
contextMenu: true,
|
||||
filterCount: 0,
|
||||
searchInputWidth: '200px',
|
||||
searchValue: '',
|
||||
|
||||
@@ -13,7 +13,6 @@ export interface CardProps {
|
||||
cardHeight?: string;
|
||||
cardClass?: string;
|
||||
cardBodyStyle?: CSSProperties;
|
||||
contextMenu?: boolean;
|
||||
filterCount?: number;
|
||||
searchInputPlaceholder?: string;
|
||||
searchInputWidth?: string;
|
||||
|
||||
@@ -8,7 +8,9 @@ import type {
|
||||
TerminalSshDisplaySetting,
|
||||
TerminalSshInteractSetting,
|
||||
TerminalSshPluginsSetting,
|
||||
TerminalState
|
||||
TerminalState,
|
||||
TerminalVncActionBarSetting,
|
||||
TerminalVncGraphSetting
|
||||
} from './types';
|
||||
import type {
|
||||
IDomViewportHandler,
|
||||
@@ -28,7 +30,7 @@ import { getPreference, updatePreference } from '@/api/user/preference';
|
||||
import { getLatestConnectHostId } from '@/api/terminal/terminal-connect-log';
|
||||
import { useCacheStore } from '@/store';
|
||||
import { nextId } from '@/utils';
|
||||
import { isObject } from '@/utils/is';
|
||||
import { isArray, isObject } from '@/utils/is';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
import { TerminalSessionTypes, TerminalTabs } from '@/views/terminal/types/const';
|
||||
import TerminalTabManager from '@/views/terminal/service/tab/terminal-tab-manager';
|
||||
@@ -56,8 +58,12 @@ export const TerminalPreferenceItem = {
|
||||
RDP_GRAPH_SETTING: 'rdpGraphSetting',
|
||||
// rdp 操作栏设置
|
||||
RDP_ACTION_BAR_SETTING: 'rdpActionBarSetting',
|
||||
// 会话设置
|
||||
// rdp 会话设置
|
||||
RDP_SESSION_SETTING: 'rdpSessionSetting',
|
||||
// vnc 图形化设置
|
||||
VNC_GRAPH_SETTING: 'vncGraphSetting',
|
||||
// vnc 工具栏设置
|
||||
VNC_ACTION_BAR_SETTING: 'vncActionBarSetting',
|
||||
// 快捷键设置
|
||||
SHORTCUT_SETTING: 'shortcutSetting',
|
||||
};
|
||||
@@ -77,6 +83,8 @@ export default defineStore('terminal', {
|
||||
rdpGraphSetting: {} as TerminalRdpGraphSetting,
|
||||
rdpSessionSetting: {} as TerminalRdpSessionSetting,
|
||||
rdpActionBarSetting: {} as TerminalRdpActionBarSetting,
|
||||
vncGraphSetting: {} as TerminalVncGraphSetting,
|
||||
vncActionBarSetting: {} as TerminalVncActionBarSetting,
|
||||
shortcutSetting: {
|
||||
enabled: false,
|
||||
keys: []
|
||||
@@ -138,7 +146,7 @@ export default defineStore('terminal', {
|
||||
await updatePreference({
|
||||
type: 'TERMINAL',
|
||||
item,
|
||||
value: isObject(value) ? JSON.stringify(value) : value,
|
||||
value: (isObject(value) || isArray(value)) ? JSON.stringify(value) : value,
|
||||
});
|
||||
} catch (e) {
|
||||
Message.error('同步失败');
|
||||
|
||||
@@ -23,6 +23,8 @@ export interface TerminalPreference {
|
||||
rdpGraphSetting: TerminalRdpGraphSetting;
|
||||
rdpActionBarSetting: TerminalRdpActionBarSetting;
|
||||
rdpSessionSetting: TerminalRdpSessionSetting;
|
||||
vncGraphSetting: TerminalVncGraphSetting;
|
||||
vncActionBarSetting: TerminalVncActionBarSetting;
|
||||
shortcutSetting: TerminalShortcutSetting;
|
||||
}
|
||||
|
||||
@@ -68,6 +70,7 @@ export interface TerminalSshInteractSetting {
|
||||
wordSeparator: string;
|
||||
terminalEmulationType: string;
|
||||
scrollBackLine: number;
|
||||
replaceBackspace: boolean;
|
||||
}
|
||||
|
||||
// RDP 图形化设置
|
||||
@@ -111,6 +114,30 @@ export interface TerminalRdpSessionSetting {
|
||||
driveMountMode?: string;
|
||||
}
|
||||
|
||||
// VNC 图形化设置
|
||||
export interface TerminalVncGraphSetting {
|
||||
displaySize?: string;
|
||||
displayWidth?: number;
|
||||
displayHeight?: number;
|
||||
colorDepth?: number;
|
||||
forceLossless?: boolean;
|
||||
cursor?: string;
|
||||
compressLevel?: number;
|
||||
qualityLevel?: number;
|
||||
}
|
||||
|
||||
// VNC 操作栏设置
|
||||
export interface TerminalVncActionBarSetting {
|
||||
position?: string;
|
||||
display?: boolean;
|
||||
combinationKey?: boolean;
|
||||
clipboard?: boolean;
|
||||
disconnect?: boolean;
|
||||
close?: boolean;
|
||||
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
// 终端快捷键设置
|
||||
export interface TerminalShortcutSetting {
|
||||
enabled: boolean;
|
||||
|
||||
@@ -30,7 +30,7 @@ const checkForVersionUpdate = (serverVersion: string) => {
|
||||
return;
|
||||
}
|
||||
// 提示用户更新
|
||||
if (window.confirm('检测到新版本, 是否刷新页面以获取最新内容?')) {
|
||||
if (window.confirm('检测到新版本, 请强制刷新页面以获取最新内容!')) {
|
||||
window.location.reload();
|
||||
}
|
||||
// 更新 localStorage 记录
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="layout-container view-container">
|
||||
<div class="layout-container view-container" v-if="render">
|
||||
<a-tabs v-model:active-key="activeKey"
|
||||
class="tabs-container simple-card"
|
||||
size="large"
|
||||
@@ -32,12 +32,14 @@
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
const render = ref();
|
||||
const activeKey = ref();
|
||||
|
||||
// 加载字典项
|
||||
onBeforeMount(async () => {
|
||||
const dictStore = useDictStore();
|
||||
await dictStore.loadKeys(dictKeys);
|
||||
render.value = true;
|
||||
});
|
||||
|
||||
// 跳转到指定页
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="layout-container">
|
||||
<div class="layout-container" v-if="render">
|
||||
<!-- 列表-表格 -->
|
||||
<host-identity-table v-if="renderTable"
|
||||
ref="table"
|
||||
@@ -36,6 +36,7 @@
|
||||
import HostIdentityFormModal from './components/host-identity-form-modal.vue';
|
||||
import HostKeyFormDrawer from '../host-key/components/host-key-form-drawer.vue';
|
||||
|
||||
const render = ref();
|
||||
const table = ref();
|
||||
const card = ref();
|
||||
const modal = ref();
|
||||
@@ -57,6 +58,7 @@
|
||||
onBeforeMount(async () => {
|
||||
const dictStore = useDictStore();
|
||||
await dictStore.loadKeys(dictKeys);
|
||||
render.value = true;
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
@@ -208,6 +208,31 @@
|
||||
<!-- 拓展操作 -->
|
||||
<template #extra="{ record }">
|
||||
<a-space>
|
||||
<!-- 单协议连接 -->
|
||||
<a-button v-if="record.types?.length === 1"
|
||||
size="mini"
|
||||
v-permission="['terminal:terminal:access']"
|
||||
@click="openNewRoute({ name: 'terminal', query: { connect: record.id, type: record.types[0] } })">
|
||||
连接
|
||||
</a-button>
|
||||
<!-- 多协议连接 -->
|
||||
<a-popover v-if="(record.types?.length || 0) > 1"
|
||||
:title="undefined"
|
||||
:content-style="{ padding: '8px' }">
|
||||
<a-button v-permission="['terminal:terminal:access']" size="mini">
|
||||
连接
|
||||
</a-button>
|
||||
<template #content>
|
||||
<a-space>
|
||||
<a-button v-for="type in record.types"
|
||||
:key="type"
|
||||
size="mini"
|
||||
@click="openNewRoute({ name: 'terminal', query: { connect: record.id, type} })">
|
||||
{{ type }}
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</a-popover>
|
||||
<!-- 更多操作 -->
|
||||
<a-dropdown trigger="hover" :popup-max-height="false">
|
||||
<icon-more class="card-extra-icon" />
|
||||
@@ -236,18 +261,6 @@
|
||||
@click="deleteRow(record.id)">
|
||||
<span class="more-doption error">删除</span>
|
||||
</a-doption>
|
||||
<!-- SSH -->
|
||||
<a-doption v-if="record.types.includes(HostType.SSH.value)"
|
||||
v-permission="['terminal:terminal:access']"
|
||||
@click="openNewRoute({ name: 'terminal', query: { connect: record.id, type: 'SSH' } })">
|
||||
<span class="more-doption normal">SSH</span>
|
||||
</a-doption>
|
||||
<!-- RDP -->
|
||||
<a-doption v-if="record.types.includes(HostType.RDP.value)"
|
||||
v-permission="['terminal:terminal:access']"
|
||||
@click="openNewRoute({ name: 'terminal', query: { connect: record.id, type: 'RDP' } })">
|
||||
<span class="more-doption normal">RDP</span>
|
||||
</a-doption>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</a-space>
|
||||
|
||||
@@ -51,6 +51,15 @@
|
||||
class="form-panel"
|
||||
:hostId="hostId" />
|
||||
</a-tab-pane>
|
||||
<!-- VNC 配置 -->
|
||||
<a-tab-pane v-permission="['asset:host:update-config']"
|
||||
key="vnc"
|
||||
title="VNC"
|
||||
:disabled="!hostId || !types.includes(HostType.VNC.value)">
|
||||
<host-form-vnc v-if="hostId"
|
||||
class="form-panel"
|
||||
:hostId="hostId" />
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</div>
|
||||
</a-drawer>
|
||||
@@ -64,14 +73,15 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, nextTick } from 'vue';
|
||||
import useVisible from '@/hooks/visible';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
import { useCacheStore } from '@/store';
|
||||
import { HostType } from '../types/const';
|
||||
import useVisible from '@/hooks/visible';
|
||||
import HostFormInfo from './host-form-info.vue';
|
||||
import HostFormSpec from './host-form-spec.vue';
|
||||
import HostFormSsh from './host-form-ssh.vue';
|
||||
import HostFormRdp from './host-form-rdp.vue';
|
||||
import HostFormVnc from './host-form-vnc.vue';
|
||||
|
||||
const { visible, setVisible } = useVisible();
|
||||
|
||||
@@ -115,8 +125,8 @@
|
||||
hostId.value = id;
|
||||
hostViewUpdated.value = false;
|
||||
types.value = [];
|
||||
checkHostGroup();
|
||||
setVisible(true);
|
||||
checkHostGroup();
|
||||
};
|
||||
|
||||
// 检查是否有主机分组
|
||||
@@ -187,5 +197,21 @@
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 0 24px;
|
||||
|
||||
:deep(.password-switch) {
|
||||
width: 148px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
:deep(.password-auth-type-group) {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
.arco-radio-button {
|
||||
width: 50%;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
label-align="right"
|
||||
:label-col-props="{ span: 6 }"
|
||||
:wrapper-col-props="{ span: 18 }"
|
||||
:rules="rdpFormRules">
|
||||
:rules="formRules">
|
||||
<!-- 端口 -->
|
||||
<a-form-item field="port"
|
||||
label="端口"
|
||||
@@ -18,7 +18,6 @@
|
||||
<!-- 用户名 -->
|
||||
<a-form-item field="username"
|
||||
label="用户名"
|
||||
:rules="usernameRules"
|
||||
:help="HostAuthType.IDENTITY === formModel.authType ? '将使用主机身份的用户名' : undefined"
|
||||
hide-asterisk>
|
||||
<a-input v-model="formModel.username"
|
||||
@@ -30,7 +29,7 @@
|
||||
label="认证方式"
|
||||
hide-asterisk>
|
||||
<a-radio-group type="button"
|
||||
class="auth-type-group usn"
|
||||
class="password-auth-type-group usn"
|
||||
v-model="formModel.authType"
|
||||
:options="toRadioOptions(passwordAuthTypeKey)" />
|
||||
</a-form-item>
|
||||
@@ -38,7 +37,6 @@
|
||||
<a-form-item v-if="HostAuthType.PASSWORD === formModel.authType"
|
||||
field="password"
|
||||
label="主机密码"
|
||||
:rules="passwordRules"
|
||||
hide-asterisk>
|
||||
<a-input-password v-model="formModel.password"
|
||||
:disabled="!formModel.useNewPassword && formModel.hasPassword"
|
||||
@@ -154,18 +152,6 @@
|
||||
@click="saveConfig">
|
||||
保存
|
||||
</a-button>
|
||||
<!-- 测试连接 -->
|
||||
<a-tooltip position="tr"
|
||||
content="请先保存后测试连接"
|
||||
mini>
|
||||
<a-button class="extra-button"
|
||||
type="primary"
|
||||
:loading="connectLoading"
|
||||
long
|
||||
@click="testConnect">
|
||||
测试连接
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-spin>
|
||||
@@ -178,7 +164,6 @@
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { FieldRule } from '@arco-design/web-vue';
|
||||
import type { HostRdpConfig } from '@/api/asset/host-config';
|
||||
import { ref, onMounted } from 'vue';
|
||||
import useLoading from '@/hooks/loading';
|
||||
@@ -186,10 +171,7 @@
|
||||
import { passwordAuthTypeKey, HostAuthType, HostType, timezoneKey, keyboardLayoutKey, clipboardNormalizeKey } from '../types/const';
|
||||
import { IdentityType } from '@/views/asset/host-identity/types/const';
|
||||
import { rdpFormRules } from '../types/form.rules';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
import { encrypt } from '@/utils/rsa';
|
||||
import { testHostConnect } from '@/api/asset/host';
|
||||
import { getHostConfig, updateHostConfig } from '@/api/asset/host-config';
|
||||
import useHostConfigForm from '../types/use-host-config';
|
||||
import HostIdentitySelector from '@/components/asset/host-identity/selector/index.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -197,130 +179,28 @@
|
||||
}>();
|
||||
|
||||
const { loading, setLoading } = useLoading();
|
||||
const { loading: connectLoading, setLoading: setConnectLoading } = useLoading();
|
||||
const { toOptions, toRadioOptions } = useDictStore();
|
||||
|
||||
const formRef = ref();
|
||||
const formModel = ref<HostRdpConfig>({} as HostRdpConfig);
|
||||
|
||||
// 用户名验证
|
||||
const usernameRules = [{
|
||||
validator: (value, cb) => {
|
||||
if (value && value.length > 128) {
|
||||
cb('用户名长度不能大于128位');
|
||||
return;
|
||||
}
|
||||
if (formModel.value.authType !== HostAuthType.IDENTITY && !value) {
|
||||
cb('请输入用户名');
|
||||
return;
|
||||
}
|
||||
}
|
||||
}] as FieldRule[];
|
||||
|
||||
// 密码验证
|
||||
const passwordRules = [{
|
||||
validator: (value, cb) => {
|
||||
if (value && value.length > 256) {
|
||||
cb('密码长度不能大于256位');
|
||||
return;
|
||||
}
|
||||
if (formModel.value.useNewPassword && !value) {
|
||||
cb('请输入密码');
|
||||
return;
|
||||
}
|
||||
}
|
||||
}] as FieldRule[];
|
||||
|
||||
// 加载配置
|
||||
const fetchHostConfig = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
// 加载配置
|
||||
const { data } = await getHostConfig<HostRdpConfig>({
|
||||
hostId: props.hostId,
|
||||
type: HostType.RDP.value,
|
||||
});
|
||||
formModel.value = data;
|
||||
// 使用新密码默认为不包含密码
|
||||
formModel.value.useNewPassword = !formModel.value.hasPassword;
|
||||
} catch ({ message }) {
|
||||
Message.error(`配置加载失败 ${message}`);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 测试连接
|
||||
const testConnect = async () => {
|
||||
// 验证参数
|
||||
const error = await formRef.value.validate();
|
||||
if (error) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
setConnectLoading(true);
|
||||
// 测试连接
|
||||
await testHostConnect({
|
||||
id: props.hostId,
|
||||
type: HostType.RDP.value,
|
||||
});
|
||||
Message.success('连接成功');
|
||||
} catch (e) {
|
||||
} finally {
|
||||
setConnectLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 保存配置
|
||||
const saveConfig = async () => {
|
||||
// 验证参数
|
||||
const error = await formRef.value.validate();
|
||||
if (error) {
|
||||
return;
|
||||
}
|
||||
// 加密参数
|
||||
const requestData = { ...formModel.value };
|
||||
try {
|
||||
requestData.password = await encrypt(formModel.value.password);
|
||||
} catch (e) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
setLoading(true);
|
||||
// 更新
|
||||
await updateHostConfig({
|
||||
hostId: props.hostId,
|
||||
type: HostType.RDP.value,
|
||||
config: JSON.stringify(requestData),
|
||||
});
|
||||
Message.success('修改成功');
|
||||
} catch (e) {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
const {
|
||||
formRef,
|
||||
formRules,
|
||||
fetchHostConfig,
|
||||
saveConfig,
|
||||
} = useHostConfigForm({
|
||||
type: HostType.RDP.value,
|
||||
hostId: props.hostId,
|
||||
rules: rdpFormRules,
|
||||
formModel,
|
||||
setLoading,
|
||||
});
|
||||
|
||||
onMounted(fetchHostConfig);
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.auth-type-group {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
:deep(.arco-radio-button) {
|
||||
width: 50%;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.password-switch {
|
||||
width: 148px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.advanced-settings {
|
||||
margin-bottom: 16px;
|
||||
|
||||
@@ -339,5 +219,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@@ -4,8 +4,9 @@
|
||||
<a-form :model="formModel"
|
||||
ref="formRef"
|
||||
label-align="right"
|
||||
:auto-label-width="true"
|
||||
:rules="sshFormRules">
|
||||
:label-col-props="{ span: 6 }"
|
||||
:wrapper-col-props="{ span: 18 }"
|
||||
:rules="formRules">
|
||||
<!-- 端口 -->
|
||||
<a-form-item field="port"
|
||||
label="端口"
|
||||
@@ -17,7 +18,6 @@
|
||||
<!-- 用户名 -->
|
||||
<a-form-item field="username"
|
||||
label="用户名"
|
||||
:rules="usernameRules"
|
||||
:help="HostAuthType.IDENTITY === formModel.authType ? '将使用主机身份的用户名' : undefined">
|
||||
<a-input v-model="formModel.username"
|
||||
:disabled="HostAuthType.IDENTITY === formModel.authType"
|
||||
@@ -35,8 +35,7 @@
|
||||
<!-- 主机密码 -->
|
||||
<a-form-item v-if="HostAuthType.PASSWORD === formModel.authType"
|
||||
field="password"
|
||||
label="主机密码"
|
||||
:rules="passwordRules">
|
||||
label="主机密码">
|
||||
<a-input-password v-model="formModel.password"
|
||||
:disabled="!formModel.useNewPassword && formModel.hasPassword"
|
||||
placeholder="主机密码" />
|
||||
@@ -105,7 +104,6 @@
|
||||
mini>
|
||||
<a-button class="extra-button"
|
||||
type="primary"
|
||||
:loading="connectLoading"
|
||||
long
|
||||
@click="testConnect">
|
||||
测试连接
|
||||
@@ -123,7 +121,6 @@
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { FieldRule } from '@arco-design/web-vue';
|
||||
import type { HostSshConfig } from '@/api/asset/host-config';
|
||||
import { ref, onMounted } from 'vue';
|
||||
import useLoading from '@/hooks/loading';
|
||||
@@ -131,9 +128,8 @@
|
||||
import { sshAuthTypeKey, HostAuthType, HostType } from '../types/const';
|
||||
import { sshFormRules } from '../types/form.rules';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
import { encrypt } from '@/utils/rsa';
|
||||
import { testHostConnect } from '@/api/asset/host';
|
||||
import { getHostConfig, updateHostConfig } from '@/api/asset/host-config';
|
||||
import useHostConfigForm from '../types/use-host-config';
|
||||
import HostIdentitySelector from '@/components/asset/host-identity/selector/index.vue';
|
||||
import HostKeySelector from '@/components/asset/host-key/selector/index.vue';
|
||||
|
||||
@@ -142,103 +138,34 @@
|
||||
}>();
|
||||
|
||||
const { loading, setLoading } = useLoading();
|
||||
const { loading: connectLoading, setLoading: setConnectLoading } = useLoading();
|
||||
const { toRadioOptions } = useDictStore();
|
||||
|
||||
const formRef = ref();
|
||||
const formModel = ref<HostSshConfig>({} as HostSshConfig);
|
||||
|
||||
// 用户名验证
|
||||
const usernameRules = [{
|
||||
validator: (value, cb) => {
|
||||
if (value && value.length > 128) {
|
||||
cb('用户名长度不能大于128位');
|
||||
return;
|
||||
}
|
||||
if (formModel.value.authType !== HostAuthType.IDENTITY && !value) {
|
||||
cb('请输入用户名');
|
||||
return;
|
||||
}
|
||||
}
|
||||
}] as FieldRule[];
|
||||
|
||||
// 密码验证
|
||||
const passwordRules = [{
|
||||
validator: (value, cb) => {
|
||||
if (value && value.length > 256) {
|
||||
cb('密码长度不能大于256位');
|
||||
return;
|
||||
}
|
||||
if (formModel.value.useNewPassword && !value) {
|
||||
cb('请输入密码');
|
||||
return;
|
||||
}
|
||||
}
|
||||
}] as FieldRule[];
|
||||
|
||||
// 加载配置
|
||||
const fetchHostConfig = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
// 加载配置
|
||||
const { data } = await getHostConfig<HostSshConfig>({
|
||||
hostId: props.hostId,
|
||||
type: HostType.SSH.value,
|
||||
});
|
||||
formModel.value = data;
|
||||
// 使用新密码默认为不包含密码
|
||||
formModel.value.useNewPassword = !formModel.value.hasPassword;
|
||||
} catch ({ message }) {
|
||||
Message.error(`配置加载失败 ${message}`);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
const {
|
||||
formRef,
|
||||
formRules,
|
||||
fetchHostConfig,
|
||||
saveConfig,
|
||||
} = useHostConfigForm({
|
||||
type: HostType.SSH.value,
|
||||
hostId: props.hostId,
|
||||
rules: sshFormRules,
|
||||
formModel,
|
||||
setLoading,
|
||||
});
|
||||
|
||||
// 测试连接
|
||||
const testConnect = async () => {
|
||||
// 验证参数
|
||||
const error = await formRef.value.validate();
|
||||
if (error) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
setConnectLoading(true);
|
||||
// 测试连接
|
||||
await testHostConnect({
|
||||
id: props.hostId,
|
||||
type: HostType.SSH.value,
|
||||
});
|
||||
Message.success('连接成功');
|
||||
} catch (e) {
|
||||
} finally {
|
||||
setConnectLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 保存配置
|
||||
const saveConfig = async () => {
|
||||
// 验证参数
|
||||
const error = await formRef.value.validate();
|
||||
if (error) {
|
||||
return;
|
||||
}
|
||||
// 加密参数
|
||||
const requestData = { ...formModel.value };
|
||||
try {
|
||||
requestData.password = await encrypt(formModel.value.password);
|
||||
} catch (e) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
setLoading(true);
|
||||
// 更新
|
||||
await updateHostConfig({
|
||||
hostId: props.hostId,
|
||||
type: HostType.SSH.value,
|
||||
config: JSON.stringify(requestData),
|
||||
});
|
||||
Message.success('修改成功');
|
||||
// 测试连接
|
||||
await testHostConnect({ id: props.hostId, type: HostType.SSH.value });
|
||||
Message.success('连接成功');
|
||||
} catch (e) {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
@@ -261,9 +188,4 @@
|
||||
}
|
||||
}
|
||||
|
||||
.password-switch {
|
||||
width: 148px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
<template>
|
||||
<a-spin :loading="loading">
|
||||
<!-- 表单 -->
|
||||
<a-form :model="formModel"
|
||||
ref="formRef"
|
||||
label-align="right"
|
||||
:label-col-props="{ span: 6 }"
|
||||
:wrapper-col-props="{ span: 18 }"
|
||||
:rules="formRules">
|
||||
<!-- 端口 -->
|
||||
<a-form-item field="port"
|
||||
label="端口"
|
||||
hide-asterisk>
|
||||
<a-input-number v-model="formModel.port"
|
||||
placeholder="请输入 VNC 端口"
|
||||
hide-button />
|
||||
</a-form-item>
|
||||
<a-row>
|
||||
<a-col :span="8">
|
||||
<!-- 无用户名 -->
|
||||
<a-form-item field="noUsername"
|
||||
label="无用户名"
|
||||
:label-col-props="{ span: 18 }"
|
||||
:wrapper-col-props="{ span: 6 }"
|
||||
hide-asterisk>
|
||||
<a-switch v-model="formModel.noUsername" type="round" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<!-- 无密码 -->
|
||||
<a-form-item field="noPassword"
|
||||
label="无密码"
|
||||
:label-col-props="{ span: 18 }"
|
||||
:wrapper-col-props="{ span: 6 }"
|
||||
hide-asterisk>
|
||||
<a-switch v-model="formModel.noPassword" type="round" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<!-- 用户名 -->
|
||||
<a-form-item v-if="formModel.noUsername !== true"
|
||||
field="username"
|
||||
label="用户名"
|
||||
:help="HostAuthType.IDENTITY === formModel.authType ? '将使用主机身份的用户名' : undefined"
|
||||
hide-asterisk>
|
||||
<a-input v-model="formModel.username"
|
||||
:disabled="HostAuthType.IDENTITY === formModel.authType"
|
||||
placeholder="请输入用户名" />
|
||||
</a-form-item>
|
||||
<!-- 认证方式 -->
|
||||
<a-form-item v-if="formModel.noPassword !== true"
|
||||
field="authType"
|
||||
label="认证方式"
|
||||
hide-asterisk>
|
||||
<a-radio-group type="button"
|
||||
class="password-auth-type-group usn"
|
||||
v-model="formModel.authType"
|
||||
:options="toRadioOptions(passwordAuthTypeKey)" />
|
||||
</a-form-item>
|
||||
<!-- 主机密码 -->
|
||||
<a-form-item v-if="formModel.noPassword !== true && HostAuthType.PASSWORD === formModel.authType"
|
||||
field="password"
|
||||
label="主机密码"
|
||||
hide-asterisk>
|
||||
<a-input-password v-model="formModel.password"
|
||||
:disabled="!formModel.useNewPassword && formModel.hasPassword"
|
||||
placeholder="主机密码" />
|
||||
<a-switch v-if="formModel.hasPassword"
|
||||
v-model="formModel.useNewPassword"
|
||||
class="password-switch"
|
||||
type="round"
|
||||
checked-text="使用新密码"
|
||||
unchecked-text="使用原密码" />
|
||||
</a-form-item>
|
||||
<!-- 主机身份 -->
|
||||
<a-form-item v-if="HostAuthType.IDENTITY === formModel.authType"
|
||||
field="identityId"
|
||||
label="主机身份"
|
||||
hide-asterisk>
|
||||
<host-identity-selector v-model="formModel.identityId"
|
||||
:type="IdentityType.PASSWORD" />
|
||||
</a-form-item>
|
||||
<!-- 系统时区 -->
|
||||
<a-form-item field="timezone"
|
||||
label="系统时区"
|
||||
hide-asterisk>
|
||||
<a-select v-model="formModel.timezone"
|
||||
placeholder="请选择系统时区"
|
||||
:options="toOptions(timezoneKey)" />
|
||||
</a-form-item>
|
||||
<!-- 剪切板编码 -->
|
||||
<a-form-item field="clipboardEncoding"
|
||||
label="剪切板编码"
|
||||
hide-asterisk>
|
||||
<a-select v-model="formModel.clipboardEncoding"
|
||||
placeholder="请选择剪切板编码"
|
||||
:options="toOptions(clipboardEncodingKey)" />
|
||||
</a-form-item>
|
||||
<!-- 操作 -->
|
||||
<a-form-item style="margin-bottom: 0;">
|
||||
<!-- 保存 -->
|
||||
<a-button type="primary"
|
||||
long
|
||||
@click="saveConfig">
|
||||
保存
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-spin>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'hostFormVnc'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { HostVncConfig } from '@/api/asset/host-config';
|
||||
import { ref, onMounted } from 'vue';
|
||||
import useLoading from '@/hooks/loading';
|
||||
import { useDictStore } from '@/store';
|
||||
import { passwordAuthTypeKey, HostAuthType, HostType, clipboardEncodingKey, timezoneKey } from '../types/const';
|
||||
import { IdentityType } from '@/views/asset/host-identity/types/const';
|
||||
import { vncFormRules } from '../types/form.rules';
|
||||
import useHostConfigForm from '../types/use-host-config';
|
||||
import HostIdentitySelector from '@/components/asset/host-identity/selector/index.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
hostId: number;
|
||||
}>();
|
||||
|
||||
const { loading, setLoading } = useLoading();
|
||||
const { toOptions, toRadioOptions } = useDictStore();
|
||||
|
||||
const formModel = ref<HostVncConfig>({} as HostVncConfig);
|
||||
|
||||
const {
|
||||
formRef,
|
||||
formRules,
|
||||
fetchHostConfig,
|
||||
saveConfig,
|
||||
} = useHostConfigForm({
|
||||
type: HostType.VNC.value,
|
||||
hostId: props.hostId,
|
||||
rules: vncFormRules,
|
||||
formModel,
|
||||
setLoading,
|
||||
});
|
||||
|
||||
onMounted(fetchHostConfig);
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
</style>
|
||||
@@ -274,7 +274,6 @@
|
||||
<a-space>
|
||||
<a-button v-for="type in record.types"
|
||||
:key="type"
|
||||
type="text"
|
||||
size="mini"
|
||||
@click="openNewRoute({ name: 'terminal', query: { connect: record.id, type} })">
|
||||
{{ type }}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="layout-container">
|
||||
<div class="layout-container" v-if="render">
|
||||
<!-- 列表-表格 -->
|
||||
<host-table v-if="renderTable"
|
||||
ref="table"
|
||||
@@ -36,6 +36,7 @@
|
||||
import HostFormDrawer from './components/host-form-drawer.vue';
|
||||
import HostGroupDrawer from '../host-group/drawer/index.vue';
|
||||
|
||||
const render = ref();
|
||||
const table = ref();
|
||||
const card = ref();
|
||||
const drawer = ref();
|
||||
@@ -57,6 +58,7 @@
|
||||
// 加载字典配置
|
||||
onBeforeMount(() => {
|
||||
useDictStore().loadKeys(dictKeys);
|
||||
render.value = true;
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
@@ -24,6 +24,10 @@ export const HostType = {
|
||||
value: 'RDP',
|
||||
port: 3389,
|
||||
},
|
||||
VNC: {
|
||||
value: 'VNC',
|
||||
port: 5900,
|
||||
},
|
||||
};
|
||||
|
||||
// 系统类型
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import type { FieldRule } from '@arco-design/web-vue';
|
||||
import type { Ref } from 'vue';
|
||||
import type { HostBaseConfig } from '@/api/asset/host-config';
|
||||
import { HostAuthType } from './const';
|
||||
|
||||
// 主机表单规则
|
||||
export const hostFormRules = {
|
||||
@@ -62,11 +65,11 @@ export const hostFormRules = {
|
||||
export const sshFormRules = {
|
||||
port: [{
|
||||
required: true,
|
||||
message: '请输入 SSH 端口'
|
||||
message: '请输入端口'
|
||||
}, {
|
||||
min: 1,
|
||||
max: 65535,
|
||||
message: 'SSH 端口不合法'
|
||||
message: '端口不合法'
|
||||
}],
|
||||
authType: [{
|
||||
required: true,
|
||||
@@ -116,11 +119,11 @@ export const sshFormRules = {
|
||||
export const rdpFormRules = {
|
||||
port: [{
|
||||
required: true,
|
||||
message: '请输入 RDP 端口'
|
||||
message: '请输入端口'
|
||||
}, {
|
||||
min: 1,
|
||||
max: 65535,
|
||||
message: 'RDP 端口不合法'
|
||||
message: '端口不合法'
|
||||
}],
|
||||
authType: [{
|
||||
required: true,
|
||||
@@ -132,4 +135,64 @@ export const rdpFormRules = {
|
||||
}],
|
||||
} as Record<string, FieldRule | FieldRule[]>;
|
||||
|
||||
// vnc 表单规则
|
||||
export const vncFormRules = {
|
||||
port: [{
|
||||
required: true,
|
||||
message: '请输入端口'
|
||||
}, {
|
||||
min: 1,
|
||||
max: 65535,
|
||||
message: '端口不合法'
|
||||
}],
|
||||
authType: [{
|
||||
required: true,
|
||||
message: '请选择认证方式'
|
||||
}],
|
||||
identityId: [{
|
||||
required: true,
|
||||
message: '请选择主机身份'
|
||||
}],
|
||||
clipboardEncoding: [{
|
||||
required: true,
|
||||
message: '请选择剪切板编码'
|
||||
}],
|
||||
} as Record<string, FieldRule | FieldRule[]>;
|
||||
|
||||
// 基础规则
|
||||
export const baseFormRules = {
|
||||
// 用户名验证规则
|
||||
username(formModel: Ref<HostBaseConfig>) {
|
||||
return [{
|
||||
validator: (value: string, cb: (msg?: string) => void) => {
|
||||
if (value && value.length > 128) {
|
||||
cb('用户名长度不能大于128位');
|
||||
return;
|
||||
}
|
||||
if (formModel.value.authType !== HostAuthType.IDENTITY && !value) {
|
||||
cb('请输入用户名');
|
||||
return;
|
||||
}
|
||||
cb();
|
||||
}
|
||||
}] as FieldRule[];
|
||||
},
|
||||
// 密码验证规则
|
||||
password(formModel: Ref<HostBaseConfig>) {
|
||||
return [{
|
||||
validator: (value: string, cb: (msg?: string) => void) => {
|
||||
if (value && value.length > 256) {
|
||||
cb('密码长度不能大于256位');
|
||||
return;
|
||||
}
|
||||
if (formModel.value.useNewPassword && !value) {
|
||||
cb('请输入密码');
|
||||
return;
|
||||
}
|
||||
cb();
|
||||
}
|
||||
}] as FieldRule[];
|
||||
}
|
||||
};
|
||||
|
||||
export default null;
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
import type { Ref } from 'vue';
|
||||
import { ref } from 'vue';
|
||||
import type { HostBaseConfig } from '@/api/asset/host-config';
|
||||
import { getHostConfig, updateHostConfig } from '@/api/asset/host-config';
|
||||
import type { FieldRule } from '@arco-design/web-vue';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
import { baseFormRules } from './form.rules';
|
||||
import { encrypt } from '@/utils/rsa';
|
||||
|
||||
// 主机配置表单信息
|
||||
export interface UseHostConfigFormOptions<T extends HostBaseConfig> {
|
||||
type: string;
|
||||
hostId: number;
|
||||
formModel: Ref<T>;
|
||||
rules?: Record<string, FieldRule | FieldRule[]>;
|
||||
setLoading: (loading: boolean) => void;
|
||||
}
|
||||
|
||||
// 使用主机配置表单
|
||||
export default function useHostConfigForm<T extends HostBaseConfig>(options: UseHostConfigFormOptions<T>) {
|
||||
const { type, hostId, formModel, rules, setLoading } = options;
|
||||
|
||||
const formRef = ref();
|
||||
const formRules = ref({ ...rules, username: baseFormRules.username(formModel), password: baseFormRules.password(formModel) });
|
||||
|
||||
// 加载配置
|
||||
const fetchHostConfig = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const { data } = await getHostConfig<T>({ hostId, type });
|
||||
data.useNewPassword = !data.hasPassword;
|
||||
formModel.value = data;
|
||||
} catch (err: any) {
|
||||
Message.error('配置加载失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 保存配置
|
||||
const saveConfig = async () => {
|
||||
if (!formRef?.value) {
|
||||
return;
|
||||
}
|
||||
const error = await formRef.value.validate();
|
||||
if (error) {
|
||||
return;
|
||||
}
|
||||
// 加密密码
|
||||
const data = { ...formModel.value };
|
||||
try {
|
||||
data.password = await encrypt(data.password);
|
||||
} catch (e) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
setLoading(true);
|
||||
// 更新
|
||||
await updateHostConfig({
|
||||
hostId,
|
||||
type,
|
||||
config: JSON.stringify(data),
|
||||
});
|
||||
Message.success('修改成功');
|
||||
} catch (e) {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
formRef,
|
||||
formRules,
|
||||
fetchHostConfig,
|
||||
saveConfig,
|
||||
};
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="layout-container upload-container">
|
||||
<div class="layout-container upload-container" v-if="render">
|
||||
<!-- 上传面板 -->
|
||||
<upload-panel ref="panel" />
|
||||
</div>
|
||||
@@ -20,12 +20,14 @@
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
const render = ref();
|
||||
const panel = ref();
|
||||
|
||||
// 加载字典值
|
||||
onMounted(async () => {
|
||||
const dictStore = useDictStore();
|
||||
await dictStore.loadKeys(dictKeys);
|
||||
render.value = true;
|
||||
});
|
||||
|
||||
// 跳转日志
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="layout-container full">
|
||||
<div class="layout-container full" v-if="render">
|
||||
<!-- 执行面板 -->
|
||||
<div v-show="!logVisible" class="panel-wrapper">
|
||||
<exec-command-panel @submit="openLog" />
|
||||
@@ -34,6 +34,7 @@
|
||||
const { visible: logVisible, setVisible: setLogVisible } = useVisible();
|
||||
const route = useRoute();
|
||||
|
||||
const render = ref();
|
||||
const log = ref();
|
||||
|
||||
// 打开日志
|
||||
@@ -56,6 +57,7 @@
|
||||
onMounted(async () => {
|
||||
const dictStore = useDictStore();
|
||||
await dictStore.loadKeys(dictKeys);
|
||||
render.value = true;
|
||||
});
|
||||
|
||||
// 跳转日志
|
||||
|
||||
@@ -31,45 +31,21 @@
|
||||
<icon-right />
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
<!-- 打开 SSH -->
|
||||
<a-tooltip v-if="handler.host?.types?.includes(TerminalSessionTypes.SSH.type)"
|
||||
position="top"
|
||||
:mini="true"
|
||||
:auto-fix-position="false"
|
||||
content-class="terminal-tooltip-content"
|
||||
arrow-class="terminal-tooltip-content"
|
||||
content="打开 SSH">
|
||||
<a-button class="combined-handler-action icon-button"
|
||||
@click="openSession(handler.host as any, TerminalSessionTypes.SSH)">
|
||||
<icon-thunderbolt />
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
<!-- 打开 SFTP -->
|
||||
<a-tooltip v-if="handler.host?.types?.includes(TerminalSessionTypes.SSH.type)"
|
||||
position="top"
|
||||
:mini="true"
|
||||
:auto-fix-position="false"
|
||||
content-class="terminal-tooltip-content"
|
||||
arrow-class="terminal-tooltip-content"
|
||||
content="打开 SFTP">
|
||||
<a-button class="combined-handler-action icon-button"
|
||||
@click="openSession(handler.host as any, TerminalSessionTypes.SFTP)">
|
||||
<icon-folder />
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
<!-- 打开 RDP -->
|
||||
<a-tooltip v-if="handler.host?.types?.includes(TerminalSessionTypes.RDP.type)"
|
||||
position="top"
|
||||
:mini="true"
|
||||
:auto-fix-position="false"
|
||||
content-class="terminal-tooltip-content"
|
||||
arrow-class="terminal-tooltip-content"
|
||||
content="打开 RDP">
|
||||
<a-button class="combined-handler-action icon-button"
|
||||
@click="openSession(handler.host as any, TerminalSessionTypes.RDP)">
|
||||
<icon-computer />
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
<!-- 打开会话 -->
|
||||
<template v-for="type in TerminalSessionTypes">
|
||||
<template v-if="handler.host?.types?.includes(type.protocol)">
|
||||
<a-tooltip position="top"
|
||||
:mini="true"
|
||||
:auto-fix-position="false"
|
||||
:content="`打开 ${type.type}`"
|
||||
content-class="terminal-tooltip-content"
|
||||
arrow-class="terminal-tooltip-content">
|
||||
<a-button class="combined-handler-action icon-button" @click="openSession(handler.host as any, type)">
|
||||
<component :is="type.connectIcon || type.icon" />
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -32,15 +32,25 @@
|
||||
<span class="tab-title-icon">
|
||||
<component :is="item.icon" />
|
||||
</span>
|
||||
{{ item.title }}
|
||||
<span>{{ item.title }}</span>
|
||||
</span>
|
||||
</template>
|
||||
<!-- ssh -->
|
||||
<ssh-view v-if="item.type === TerminalSessionTypes.SSH.type" :item="item" />
|
||||
<ssh-view v-if="item.type === TerminalSessionTypes.SSH.type"
|
||||
class="session-container"
|
||||
:item="item" />
|
||||
<!-- sftp -->
|
||||
<sftp-view v-else-if="item.type === TerminalSessionTypes.SFTP.type" :item="item" />
|
||||
<sftp-view v-else-if="item.type === TerminalSessionTypes.SFTP.type"
|
||||
class="session-container"
|
||||
:item="item" />
|
||||
<!-- rdp -->
|
||||
<rdp-view v-else-if="item.type === TerminalSessionTypes.RDP.type" :item="item" />
|
||||
<rdp-view v-else-if="item.type === TerminalSessionTypes.RDP.type"
|
||||
class="session-container"
|
||||
:item="item" />
|
||||
<!-- vnc -->
|
||||
<vnc-view v-else-if="item.type === TerminalSessionTypes.VNC.type"
|
||||
class="session-container"
|
||||
:item="item" />
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</div>
|
||||
@@ -60,6 +70,7 @@
|
||||
import SshView from '../view/ssh/ssh-view.vue';
|
||||
import SftpView from '../view/sftp/sftp-view.vue';
|
||||
import RdpView from '../view/rdp/rdp-view.vue';
|
||||
import VncView from '../view/vnc/vnc-view.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
index: number;
|
||||
@@ -118,56 +129,62 @@
|
||||
.terminal-panel-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.tab-title-wrapper {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 11px 18px 9px 14px;
|
||||
background: var(--bg);
|
||||
position: relative;
|
||||
transition: all .3s;
|
||||
|
||||
.tab-title-icon {
|
||||
font-size: 16px;
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
filter: brightness(1.04);
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
width: calc(100% - 3px);
|
||||
height: 2px;
|
||||
background: var(--color);
|
||||
position: absolute;
|
||||
left: 1px;
|
||||
bottom: -1px;
|
||||
}
|
||||
}
|
||||
|
||||
.panel-extra {
|
||||
margin-right: 8px;
|
||||
|
||||
.extra-icon {
|
||||
color: var(--color-panel-text-1);
|
||||
transition: 0.2s;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
.tab-title-wrapper {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
padding: 4px 18px 4px 14px;
|
||||
background: var(--bg);
|
||||
position: relative;
|
||||
transition: all .3s;
|
||||
|
||||
.tab-title-icon {
|
||||
font-size: 16px;
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: var(--color-bg-panel-icon-1);
|
||||
filter: brightness(1.04);
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
width: calc(100% - 3px);
|
||||
height: 2px;
|
||||
background: var(--color);
|
||||
position: absolute;
|
||||
left: 1px;
|
||||
bottom: -1px;
|
||||
}
|
||||
}
|
||||
|
||||
.panel-extra {
|
||||
margin-right: 8px;
|
||||
|
||||
.extra-icon {
|
||||
color: var(--color-panel-text-1);
|
||||
transition: 0.2s;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
|
||||
&:hover {
|
||||
background: var(--color-bg-panel-icon-1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.session-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user