Compare commits

...

81 Commits

Author SHA1 Message Date
ljw
3679fcc874 fix group create type 2024-10-20 20:15:12 +08:00
ljw
d085b4e3c2 up readme 2024-10-20 19:40:49 +08:00
dc8fcdf214 Merge pull request #17 from Ogannesson/master
Add proxy option for Google Oauthon
2024-10-20 19:05:01 +08:00
Oganneson
8bab23b65b Add oauth callback via proxy
Improved support for environment variables and configuration files, and standardized default behaviors
2024-10-20 17:56:11 +08:00
ljw
f64022e411 add conn log 2024-10-18 15:05:58 +08:00
ljw
2d37302cf9 fix write when heartbeat #14 2024-10-17 10:24:02 +08:00
ljw
1a1856257d fix write when heartbeat #14 2024-10-16 21:32:55 +08:00
ljw
24f570b64f fix pc add #13 2024-10-16 10:10:20 +08:00
ljw
6322177b71 fix pc add #13 2024-10-16 09:28:39 +08:00
ljw
d2390d1cb3 Revert "add webclient v2 preview"
This reverts commit 399c32da7d.
2024-10-15 16:35:34 +08:00
ljw
6fe6f6b708 Revert "up readme"
This reverts commit a656f4fec3.
2024-10-15 16:35:34 +08:00
ljw
a656f4fec3 up readme 2024-10-15 16:13:42 +08:00
ljw
399c32da7d add webclient v2 preview 2024-10-15 16:11:40 +08:00
ljw
62167836dc fix docs 2024-10-15 14:51:19 +08:00
ljw
caef3897a0 add login fail warn &
add web client on/off &
up admin peer filter &
upgrade web client
2024-10-14 10:43:29 +08:00
ljw
5ef6810e3f up readme 2024-10-12 10:02:04 +08:00
ljw
ae2079f583 Revert "Revert "up readme""
This reverts commit aa65382a0f.
2024-10-11 22:45:35 +08:00
ljw
aa65382a0f Revert "up readme"
This reverts commit 18c61a2bfc.
2024-10-11 22:06:20 +08:00
ljw
18c61a2bfc up readme 2024-10-11 21:39:58 +08:00
ljw
7cc1a8a58a up readme 2024-10-11 10:11:42 +08:00
ljw
cf9feac702 up readme 2024-10-10 13:00:29 +08:00
ljw
a963cd0209 fix readme 2024-10-10 12:40:54 +08:00
ljw
6a5408f9b8 up gorm logger & add share to guest by web client 2024-10-09 15:53:08 +08:00
ljw
9aad62d1e4 build tag 2024-09-29 12:47:04 +08:00
ljw
867eab40f8 build default push to docker 2024-09-29 12:39:33 +08:00
ljw
eb5c7efc4c fix build 2024-09-29 12:23:34 +08:00
857abc16e7 Merge pull request #5 from gigaion/new-build-1
build.yml - Add GHCR & Dynamic Inputs
2024-09-29 12:12:17 +08:00
ljw
28b9866c42 upgrade: init by i18n
add: batch delete peer
add: batch peer to addressbook
2024-09-29 11:53:58 +08:00
Gigaion
a27deb0a41 build.yml - Add GHCR & Dynamic Inputs
build.yml - Add GHCR & Dynamic Inputs
2024-09-28 13:30:44 -07:00
ljw
8e026de20b test 2024-09-28 11:01:23 +08:00
ljw
718ecc2372 test 2024-09-28 10:12:56 +08:00
ljw
56d46722f4 test 2024-09-28 10:05:04 +08:00
ljw
b6463cd715 test 2024-09-28 09:57:32 +08:00
ljw
bd3ae0cbfe fix build docker image 2024-09-28 09:39:26 +08:00
ljw
83c3aa894f fix build docker image 2024-09-28 09:33:59 +08:00
ljw
1b88d26fea up build 2024-09-27 22:08:36 +08:00
ljw
588287fdb4 up build 2024-09-27 22:06:04 +08:00
ljw
688e544b07 up build 2024-09-27 22:03:06 +08:00
ljw
3e3f812e83 up build 2024-09-27 21:48:48 +08:00
ljw
b551c7abe4 up build 2024-09-27 21:37:12 +08:00
ljw
6d1e7a4c05 up build 2024-09-27 21:29:19 +08:00
ljw
3341a4bc8e up build docker echo manifest 2024-09-27 21:20:54 +08:00
ljw
1c84980d36 up build docker 2024-09-27 20:10:17 +08:00
ljw
833b25881d up build docker 2024-09-27 20:06:49 +08:00
ljw
9dbf58903c up build docker 2024-09-27 19:40:48 +08:00
ljw
ad007f0d91 up build docker 2024-09-27 18:49:22 +08:00
ljw
4b06973a52 up build docker 2024-09-27 18:48:09 +08:00
ljw
159a67f15d up build docker 2024-09-27 17:49:17 +08:00
ljw
7c03b9953b up build docker 2024-09-27 17:32:25 +08:00
ljw
f90987de8d up build docker 2024-09-27 17:26:40 +08:00
ljw
70e4ff7820 up build docker 2024-09-27 17:25:30 +08:00
ljw
a99356f54b up build docker 2024-09-27 17:24:25 +08:00
ljw
c5bc9534cc up build docker 2024-09-27 17:15:06 +08:00
ljw
a40733424f up build docker 2024-09-27 16:52:02 +08:00
ljw
a937efc60e up build docker 2024-09-27 16:07:06 +08:00
ljw
6adb0e8415 up build docker 2024-09-27 15:59:12 +08:00
ljw
ff9ffb2f12 up build docker 2024-09-27 15:31:43 +08:00
ljw
9be4f472ae up build docker 2024-09-27 15:23:19 +08:00
ljw
8581d74b08 up build docker 2024-09-27 15:18:45 +08:00
ljw
dafe9bd6b6 up build docker 2024-09-27 14:58:17 +08:00
ljw
3ae5772360 up build docker 2024-09-27 14:54:11 +08:00
ljw
4628dbccfb up build docker 2024-09-27 14:46:39 +08:00
ljw
572b1d4c14 up build docker 2024-09-27 14:35:43 +08:00
ljw
bdb70e9859 up build docker 2024-09-27 14:29:55 +08:00
ljw
38bda17271 up build docker 2024-09-27 14:24:17 +08:00
ljw
455e1d2e5b up build docker 2024-09-27 14:19:56 +08:00
ljw
89cd724bab up build docker 2024-09-27 14:18:36 +08:00
ljw
b9109b4d0e up build docker 2024-09-27 14:16:59 +08:00
ljw
945958f552 up build docker 2024-09-27 14:10:50 +08:00
ljw
78eb0d5c06 up release_arm64.yml 2024-09-27 10:43:48 +08:00
ljw
bc6eae711e up release_arm64.yml 2024-09-27 10:38:14 +08:00
ljw
f0a4bf6164 up docker_arm64.yml 2024-09-26 14:36:15 +08:00
ljw
fc3b5e3ac3 up docker_arm64.yml 2024-09-26 14:33:01 +08:00
ljw
f7235ac847 up docker_arm64.yml 2024-09-26 14:26:15 +08:00
ljw
231f4ddb7f up docker_arm64.yml 2024-09-26 14:24:14 +08:00
ljw
3cad3994cb add docker_arm64.yml 2024-09-26 14:18:57 +08:00
ljw
8c97cc8686 up README 2024-09-26 13:38:43 +08:00
ljw
7ae976ee5d up README_EN.md 2024-09-26 13:21:17 +08:00
ljw
e91b53eb32 test release_arm64.yml 2024-09-26 11:49:11 +08:00
ljw
90311536a7 test release_arm64.yml 2024-09-26 11:46:17 +08:00
ljw
e951b7f2f9 fix Dockerfile 2024-09-26 09:29:02 +08:00
75 changed files with 3766 additions and 2041 deletions

290
.github/workflows/build.yml vendored Normal file
View File

@@ -0,0 +1,290 @@
name: Build
on:
workflow_dispatch:
inputs:
BASE_IMAGE_NAMESPACE:
description: 'Base image namespace (Default: Your Github username)'
required: false
default: ''
DOCKERHUB_IMAGE_NAMESPACE:
description: 'Docker Hub image namespace (Default: Your Github username)'
required: false
default: ''
GHCR_IMAGE_NAMESPACE:
description: 'GitHub Container Registry image namespace (Default: Your Github username)'
required: false
default: ''
SKIP_DOCKER_HUB:
description: 'Set to true to skip pushing to Docker Hub (default: false)'
required: false
default: 'false'
SKIP_GHCR:
description: 'Set to true to skip pushing to GHCR (default: false)'
required: false
default: 'false'
WEBCLIENT_SOURCE_LOCATION:
description: 'Web Client API Repository'
required: true
default: 'https://github.com/lejianwen/rustdesk-api-web'
push:
tags:
- 'v*.*.*' # 当推送带有版本号的 tag例如 v1.0.0)时触发工作流
- 'test*'
env:
LATEST_TAG: latest
WEBCLIENT_SOURCE_LOCATION: ${{ github.event.inputs.WEBCLIENT_SOURCE_LOCATION || 'https://github.com/lejianwen/rustdesk-api-web' }}
BASE_IMAGE_NAMESPACE: ${{ github.event.inputs.BASE_IMAGE_NAMESPACE || github.actor }}
DOCKERHUB_IMAGE_NAMESPACE: ${{ github.event.inputs.DOCKERHUB_IMAGE_NAMESPACE || github.actor }}
GHCR_IMAGE_NAMESPACE: ${{ github.event.inputs.GHCR_IMAGE_NAMESPACE || github.actor }}
SKIP_DOCKER_HUB: ${{ github.event.inputs.SKIP_DOCKER_HUB || 'false' }}
SKIP_GHCR: ${{ github.event.inputs.SKIP_GHCR }}
jobs:
build:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
job:
- { platform: "amd64", goos: "linux" }
- { platform: "arm64", goos: "linux" }
- { platform: "amd64", goos: "windows" }
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Go environment
uses: actions/setup-go@v4
with:
go-version: '1.22' # 选择 Go 版本
- name: Set up npm
uses: actions/setup-node@v2
with:
node-version: '20'
- name: build rustdesk-api-web
run: |
git clone ${{ env.WEBCLIENT_SOURCE_LOCATION }}
cd rustdesk-api-web
npm install
npm run build
mkdir ../resources/admin/ -p
cp -ar dist/* ../resources/admin/
- name: tidy
run: go mod tidy
- name: swag
run: |
go install github.com/swaggo/swag/cmd/swag@latest
swag init -g cmd/apimain.go --output docs/api --instanceName api --exclude http/controller/admin
swag init -g cmd/apimain.go --output docs/admin --instanceName admin --exclude http/controller/api
- name: Build for ${{ matrix.job.goos }}-${{ matrix.job.platform }}
run: |
mkdir release -p
cp -ar resources release/
cp -ar docs release/
cp -ar conf release/
mkdir -p release/data
mkdir -p release/runtime
if [ "${{ matrix.job.goos }}" = "windows" ]; then
sudo apt-get install gcc-mingw-w64-x86-64 zip -y
GOOS=${{ matrix.job.goos }} GOARCH=${{ matrix.job.platform }} CC=x86_64-w64-mingw32-gcc CGO_LDFLAGS="-static" CGO_ENABLED=1 go build -ldflags "-s -w" -o ./release/apimain.exe ./cmd/apimain.go
zip -r ${{ matrix.job.goos}}-${{ matrix.job.platform }}.zip ./release
else
if [ "${{ matrix.job.platform }}" = "arm64" ]; then
wget https://musl.cc/aarch64-linux-musl-cross.tgz
tar -xf aarch64-linux-musl-cross.tgz
export PATH=$PATH:$PWD/aarch64-linux-musl-cross/bin
GOOS=${{ matrix.job.goos }} GOARCH=${{ matrix.job.platform }} CC=aarch64-linux-musl-gcc CGO_LDFLAGS="-static" CGO_ENABLED=1 go build -ldflags "-s -w" -o ./release/apimain ./cmd/apimain.go
else
sudo apt-get install musl musl-dev musl-tools -y
GOOS=${{ matrix.job.goos }} GOARCH=${{ matrix.job.platform }} CC=musl-gcc CGO_LDFLAGS="-static" CGO_ENABLED=1 go build -ldflags "-s -w" -o ./release/apimain ./cmd/apimain.go
fi
tar -czf ${{ matrix.job.goos}}-${{ matrix.job.platform }}.tar.gz ./release
fi
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: rustdesk-api-${{ matrix.job.goos }}-${{ matrix.job.platform }}
path: |
${{ matrix.job.goos}}-${{ matrix.job.platform }}.tar.gz
${{ matrix.job.goos}}-${{ matrix.job.platform }}.zip
- name: Upload to GitHub Release
uses: softprops/action-gh-release@v2
with:
files: |
${{ matrix.job.goos}}-${{ matrix.job.platform }}.tar.gz
${{ matrix.job.goos}}-${{ matrix.job.platform }}.zip
# tag_name: ${{ env.LATEST_TAG }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
docker:
name: Push Docker Image
needs: build
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
job:
- { platform: "amd64", goos: "linux", docker_platform: "linux/amd64" }
- { platform: "arm64", goos: "linux", docker_platform: "linux/arm64" }
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Log in to Docker Hub
if: ${{ env.SKIP_DOCKER_HUB == 'false' }} # Only log in if SKIP_DOCKER_HUB is false
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_ACCESS_TOKEN }}
- name: Log in to GitHub Container Registry
if: ${{ env.SKIP_GHCR == 'false' }} # Only log in if GHCR push is enabled
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract version from tag
id: vars
run: |
if [[ "${GITHUB_REF}" == refs/tags/* ]]; then
echo "TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
else
echo "TAG=latest" >> $GITHUB_ENV # Default to 'latest' if not a tag
fi
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v4
with:
images: ${{ env.BASE_IMAGE_NAMESPACE }}/rustdesk-api
- name: Download binaries
uses: actions/download-artifact@v4
with:
name: rustdesk-api-${{ matrix.job.goos }}-${{ matrix.job.platform }}
path: ./
- name: Unzip binaries
run: |
mkdir -p ${{ matrix.job.platform }}
tar -xzf ${{ matrix.job.goos }}-${{ matrix.job.platform }}.tar.gz -C ${{ matrix.job.platform }}
file ${{ matrix.job.platform }}/apimain
- name: Build and push Docker image to Docker Hub ${{ matrix.job.platform }}
if: ${{ env.SKIP_DOCKER_HUB == 'false' }} # Only run this step if SKIP_DOCKER_HUB is false
uses: docker/build-push-action@v5
with:
context: "."
file: ./Dockerfile
platforms: ${{ matrix.job.docker_platform }}
push: true
provenance: false
build-args: |
BUILDARCH=${{ matrix.job.platform }}
tags: |
${{ env.DOCKERHUB_IMAGE_NAMESPACE }}/rustdesk-api:${{ env.LATEST_TAG }}-${{ matrix.job.platform }},
${{ env.DOCKERHUB_IMAGE_NAMESPACE }}/rustdesk-api:${{ env.TAG }}-${{ matrix.job.platform }}
labels: ${{ steps.meta.outputs.labels }}
- name: Build and push Docker image to GHCR ${{ matrix.job.platform }}
if: ${{ env.SKIP_GHCR == 'false' }} # Only run this step if SKIP_GHCR is false
uses: docker/build-push-action@v5
with:
context: "."
file: ./Dockerfile
platforms: ${{ matrix.job.docker_platform }}
push: true
provenance: false
build-args: |
BUILDARCH=${{ matrix.job.platform }}
tags: |
ghcr.io/${{ env.GHCR_IMAGE_NAMESPACE }}/rustdesk-api:${{ env.LATEST_TAG }}-${{ matrix.job.platform }},
ghcr.io/${{ env.GHCR_IMAGE_NAMESPACE }}/rustdesk-api:${{ env.TAG }}-${{ matrix.job.platform }}
labels: ${{ steps.meta.outputs.labels }}
#
docker-manifest:
name: Push Docker Manifest
needs: docker
runs-on: ubuntu-latest
steps:
- name: Extract version from tag
id: vars
run: |
if [[ "${GITHUB_REF}" == refs/tags/* ]]; then
echo "TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
else
echo "TAG=latest" >> $GITHUB_ENV # Default to 'latest' if not a tag
fi
- name: Log in to Docker Hub
if: ${{ env.SKIP_DOCKER_HUB == 'false' }} # Only log in if Docker Hub push is enabled
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_ACCESS_TOKEN }}
- name: Log in to GitHub Container Registry
if: ${{ env.SKIP_GHCR == 'false' }} # Only log in if GHCR push is enabled
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Create and push manifest Docker Hub (:version)
if: ${{ env.SKIP_DOCKER_HUB == 'false' }}
uses: Noelware/docker-manifest-action@master
with:
base-image: ${{ env.BASE_IMAGE_NAMESPACE }}/rustdesk-api:${{ env.TAG }}
extra-images: ${{ env.DOCKERHUB_IMAGE_NAMESPACE }}/rustdesk-api:${{ env.TAG }}-amd64,
${{ env.DOCKERHUB_IMAGE_NAMESPACE }}/rustdesk-api:${{ env.TAG }}-arm64
push: true
- name: Create and push manifest GHCR (:version)
if: ${{ env.SKIP_GHCR == 'false' }}
uses: Noelware/docker-manifest-action@master
with:
base-image: ghcr.io/${{ env.BASE_IMAGE_NAMESPACE }}/rustdesk-api:${{ env.TAG }}
extra-images: ghcr.io/${{ env.GHCR_IMAGE_NAMESPACE }}/rustdesk-api:${{ env.TAG }}-amd64,
ghcr.io/${{ env.GHCR_IMAGE_NAMESPACE }}/rustdesk-api:${{ env.TAG }}-arm64
push: true
amend: true
- name: Create and push manifest Docker Hub (:latest)
if: ${{ env.SKIP_DOCKER_HUB == 'false' }}
uses: Noelware/docker-manifest-action@master
with:
base-image: ${{ env.BASE_IMAGE_NAMESPACE }}/rustdesk-api:latest
extra-images: ${{ env.DOCKERHUB_IMAGE_NAMESPACE }}/rustdesk-api:latest-amd64,
${{ env.DOCKERHUB_IMAGE_NAMESPACE }}/rustdesk-api:latest-arm64
push: true
- name: Create and push manifest GHCR (:latest)
if: ${{ env.SKIP_GHCR == 'false' }}
uses: Noelware/docker-manifest-action@master
with:
base-image: ghcr.io/${{ env.BASE_IMAGE_NAMESPACE }}/rustdesk-api:latest
extra-images: ghcr.io/${{ env.GHCR_IMAGE_NAMESPACE }}/rustdesk-api:latest-amd64,
ghcr.io/${{ env.GHCR_IMAGE_NAMESPACE }}/rustdesk-api:latest-arm64
push: true
amend: true

View File

@@ -1,38 +0,0 @@
name: Build and Push Docker Image
on:
push:
tags:
- 'v*.*.*' # 仅当推送标签(例如 v1.0.0)时触发
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_ACCESS_TOKEN }}
- name: Extract version from tag
id: vars
run: echo "TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
- name: Build and push Docker image
id: push
uses: docker/build-push-action@v6
with:
context: .
file: ./Dockerfile
push: true
tags: lejianwen/rustdesk-api:latest, lejianwen/rustdesk-api:${{ env.TAG }}

View File

@@ -1,27 +0,0 @@
# This workflow will build a golang project
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go
name: Go
on:
pull_request:
branches: [ "master" ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: '1.22'
- name: mod tidy
run: go mod tidy
- name: Build
run: go build -v -o release/apimain cmd/apimain.go
- name: Test
run: go test -v cmd/apimain.go

View File

@@ -1,9 +1,9 @@
name: Build and Release name: Build and Release
on: on:
push: workflow_dispatch:
tags: # tags:
- 'v*.*.*' # 当推送带有版本号的 tag例如 v1.0.0)时触发工作流 # - 'v*.*.*' # 当推送带有版本号的 tag例如 v1.0.0)时触发工作流
#on: #on:
# push: # push:
# branches: [ "master" ] # branches: [ "master" ]

View File

@@ -1,35 +1,11 @@
FROM golang:1.22-alpine as builder
RUN set -eux; \
apk add --no-cache git gcc build-base sqlite-dev npm nodejs; \
git clone https://github.com/lejianwen/rustdesk-api-web; \
git clone https://github.com/lejianwen/rustdesk-api; \
#先编译后台
cd rustdesk-api-web; \
npm install; \
npm run build; \
cd ..; \
mkdir -p rustdesk-api/resources/admin; \
cp -ar rustdesk-api-web/dist/* rustdesk-api/resources/admin; \
cd rustdesk-api; \
go mod tidy; \
go install github.com/swaggo/swag/cmd/swag@latest; \
swag init -g cmd/apimain.go --output docs/api --instanceName api --exclude http/controller/admin; \
swag init -g cmd/apimain.go --output docs/admin --instanceName admin --exclude http/controller/api; \
go env -w GO111MODULE=on;\
CGO_LDFLAGS="-static" CGO_ENABLED=1 go build -ldflags "-s -w" -o ./release/apimain ./cmd/apimain.go; \
cp -ar resources release/; \
mkdir -p release/resources/public; \
cp -ar docs release/; \
cp -ar conf release/; \
mkdir -p release/data; \
mkdir -p release/runtime;
VOLUME /app/data
FROM alpine FROM alpine
ARG BUILDARCH
WORKDIR /app WORKDIR /app
RUN apk add --no-cache tzdata RUN apk add --no-cache tzdata file
COPY --from=builder /go/rustdesk-api/release /app/ COPY ./${BUILDARCH}/release /app/
RUN file /app/apimain
VOLUME /app/data
EXPOSE 21114 EXPOSE 21114
CMD ["./apimain"] CMD ["./apimain"]

461
README.md
View File

@@ -9,8 +9,7 @@
<img src="https://img.shields.io/badge/gin-v1.9.0-lightBlue"/> <img src="https://img.shields.io/badge/gin-v1.9.0-lightBlue"/>
<img src="https://img.shields.io/badge/gorm-v1.25.7-green"/> <img src="https://img.shields.io/badge/gorm-v1.25.7-green"/>
<img src="https://img.shields.io/badge/swag-v1.16.3-yellow"/> <img src="https://img.shields.io/badge/swag-v1.16.3-yellow"/>
<img src="https://github.com/lejianwen/rustdesk-api/actions/workflows/release.yml/badge.svg"/> <img src="https://github.com/lejianwen/rustdesk-api/actions/workflows/build.yml/badge.svg"/>
<img src="https://github.com/lejianwen/rustdesk-api/actions/workflows/docker.yml/badge.svg"/>
</div> </div>
# 特性 # 特性
@@ -31,33 +30,62 @@
- Oauth 管理 - Oauth 管理
- 快速使用web client - 快速使用web client
- i18n - i18n
- 通过 web client 分享给游客
- Web Client - Web Client
- 自动获取API server - 自动获取API server
- 自动获取ID服务器和KEY - 自动获取ID服务器和KEY
- 自动获取地址簿 - 自动获取地址簿
- 游客通过临时分享链接直接远程到设备
## 使用前准备 ## 使用前准备
### [Rustdesk](https://github.com/rustdesk/rustdesk) ### [Rustdesk](https://github.com/rustdesk/rustdesk)
1. PC客户端使用的是 ***1.3.0***,经测试 ***1.2.6+*** 都可以 #### PC客户端使用的是 ***1.3.0***,经测试 ***1.2.6+*** 都可以
2. server端必须指定key不能用自带的生成的key,否则可能链接不上或者超时
```bash #### 关于PC端链接超时或者链接不上的问题以及解决方案
hbbs -r <relay-server-ip[:port]> -k <key> ##### 链接不上是或者超时
hbbr -k <key> 因为server端相对于客户端落后版本server不会响应客户端的`secure_tcp`请求,所以客户端超时。
``` 相关代码代码位置在`https://github.com/rustdesk/rustdesk/blob/master/src/client.rs#L322`
```rust
比如 if !key.is_empty() && !token.is_empty() {
// mainly for the security of token
```bash allow_err!(secure_tcp(&mut socket, key).await);
hbbs -r <relay-server-ip[:port]> -k abc1234567 }
hbbr -k abc1234567 ```
``` 可看到当`key`和`token`都不为空时,会调用`secure_tcp`但是server端不会响应所以客户端超时
`secure_tcp` 代码位置在 `https://github.com/rustdesk/rustdesk/blob/master/src/common.rs#L1203`
##### 4种解决方案
1. server端指定key。
- 优点:简单
- 缺点:链接不是加密的
```bash
hbbs -r <relay-server-ip[:port]> -k <key>
hbbr -k <key>
```
比如
```bash
hbbs -r <relay-server-ip[:port]> -k abc1234567
hbbr -k abc1234567
```
2. server端使用系统生成的key或者自定义的密钥对但如果client已登录链接时容易超时或者链接不上可以退出登录后再链接就可以了webclient可以不用退出登录
- 优点:链接加密
- 缺点:操作麻烦
3. server端使用系统生成的key或者自定义的密钥对fork官方客户端的代码将`secure_tcp`修改成直接返回,然后通过`Github Actions`编译,下载编译后的客户端。
参考[官方文档](https://rustdesk.com/docs/en/dev/build/all/)
- 优点:链接加密,可以自定义客户端一些功能,编译后直接可用
- 缺点需要自己fork代码编译有点难度
4. 使用[我fork的代码](https://github.com/lejianwen/rustdesk),已经修改了`secure_tcp`,可以直接下载使用,[下载地址](https://github.com/lejianwen/rustdesk/releases)
- 优点:代码改动可查看,`Github Actions`编译,链接加密,直接下载使用
- 缺点:可能跟不上官方版本更新
***对链接加密要求不高的可以使用`1`,对链接加密要求高的可以使用`3`或`4`***
## 功能 ## 功能
### API 服务: 基本实现了PC端基础的接口。支持Personal版本接口可以通过配置文件`rustdesk.personal`或环境变量`RUSTDESK_API_RUSTDESK_PERSONAL`来控制是否启用 ### API 服务
基本实现了PC端基础的接口。支持Personal版本接口可以通过配置文件`rustdesk.personal`或环境变量`RUSTDESK_API_RUSTDESK_PERSONAL`来控制是否启用
#### 登录 #### 登录
@@ -70,28 +98,27 @@
![pc_ab](docs/pc_ab.png) ![pc_ab](docs/pc_ab.png)
#### 群组,群组分为`共享组`和`普通组`,共享组中所有人都能看到小组成员的设备,普通组只有管理员能看到所有小组成员的设备 #### 群组
群组分为`共享组`和`普通组`,共享组中所有人都能看到小组成员的设备,普通组只有管理员能看到所有小组成员的设备
![pc_gr](docs/pc_gr.png) ![pc_gr](docs/pc_gr.png)
### Web Admin: ### Web Admin:
** * 使用前后端分离,提供用户友好的管理界面,主要用来管理和展示。前端代码在[rustdesk-api-web](https://github.com/lejianwen/rustdesk-api-web)
*使用前后端分离,提供用户友好的管理界面,主要用来管理和展示。前端代码在[rustdesk-api-web](https://github.com/lejianwen/rustdesk-api-web)
***
***后台访问地址是`http://<your server>[:port]/_admin/`初次安装管理员为用户名密码为`admin` `admin`,请即时更改密码*** * 后台访问地址是`http://<your server>[:port]/_admin/`初次安装管理员为用户名密码为`admin` `admin`,请即时更改密码
1. 管理员界面 1. 管理员界面
![web_admin](docs/web_admin.png) ![web_admin](docs/web_admin.png)
2. 普通用户界面 2. 普通用户界面
![web_user](docs/web_admin_user.png) ![web_user](docs/web_admin_user.png)
右上角可以更改密码 右上角可以更改密码,也可以切换语言
![web_resetpwd](docs/web_resetpwd.png) ![web_resetpwd](docs/web_resetpwd.png)
3. 分组可以自定义,方便管理,暂时支持两种类型: `共享组` 和 `普通组` 3. 分组可以自定义,方便管理,暂时支持两种类型: `共享组` 和 `普通组`
![web_admin_gr](docs/web_admin_gr.png) ![web_admin_gr](docs/web_admin_gr.png)
4. 可以直接打开webclient方便使用 4. 可以直接打开webclient方便使用也可以分享给游客游客可以直接通过webclient远程到设备
![web_webclient](docs/admin_webclient.png) ![web_webclient](docs/admin_webclient.png)
5. Oauth,暂时只支持了`Github`和`Google`, 需要创建一个`OAuth App`,然后配置到后台 5. Oauth,暂时只支持了`Github`和`Google`, 需要创建一个`OAuth App`,然后配置到后台
![web_admin_oauth](docs/web_admin_oauth.png) ![web_admin_oauth](docs/web_admin_oauth.png)
@@ -118,10 +145,14 @@
### 相关配置 ### 相关配置
* 参考`conf/config.yaml`配置文件,修改相关配置。如果`gorm.type`是`sqlite`则不需要配置mysql相关配置。 * 参考`conf/config.yaml`配置文件,修改相关配置。
* 如果`gorm.type`是`sqlite`则不需要配置mysql相关配置。
* 语言如果不设置默认为`zh-CN`
```yaml ```yaml
lang: "en" lang: "en"
app:
web-client: 1 # 1:启用 0:禁用
gin: gin:
api-addr: "0.0.0.0:21114" api-addr: "0.0.0.0:21114"
mode: "release" mode: "release"
@@ -142,54 +173,66 @@ rustdesk:
api-server: "http://192.168.1.66:21114" api-server: "http://192.168.1.66:21114"
key: "123456789" key: "123456789"
personal: 1 personal: 1
logger:
path: "./runtime/log.txt"
level: "warn" #trace,debug,info,warn,error,fatal
report-caller: true
proxy:
enable: false
host: ""
``` ```
* 环境变量,变量名前缀是RUSTDESK_API环境变量如果存在将覆盖配置文件中的配置 ### 环境变量
变量名前缀是`RUSTDESK_API`,环境变量如果存在将覆盖配置文件中的配置
| 变量名 | 说明 | 示例 | | 变量名 | 说明 | 示例 |
|-------------------------------------|--------------------------------------|-----------------------------| |------------------------------------|--------------------------------------|-----------------------------|
| TZ | 时区 | Asia/Shanghai | | TZ | 时区 | Asia/Shanghai |
| RUSTDESK_API_LANG | 语言 | `en`,`zh-CN` | | RUSTDESK_API_LANG | 语言 | `en`,`zh-CN` |
| -----GIN配置----- | ---------- | ---------- | | RUSTDESK_API_APP_WEB_CLIENT | 是否启用web-client; 1:启用,0:不启用; 默认启用 | 1 |
| RUSTDESK_API_GIN_TRUST_PROXY | 信任的代理IP列表以`,`分割,默认信任所有 | 192.168.1.2,192.168.1.3 | | -----GIN配置----- | ---------- | ---------- |
| -----------GORM配置------------------ | ------------------------------------ | --------------------------- | | RUSTDESK_API_GIN_TRUST_PROXY | 信任的代理IP列表以`,`分割,默认信任所有 | 192.168.1.2,192.168.1.3 |
| RUSTDESK_API_GORM_TYPE | 数据库类型sqlite或者mysql默认sqlite | sqlite | | -----------GORM配置---------------- | ------------------------------------ | --------------------------- |
| RUSTDESK_API_GORM_MAX_IDLE_CONNS | 数据库最大空闲连接数 | 10 | | RUSTDESK_API_GORM_TYPE | 数据库类型sqlite或者mysql默认sqlite | sqlite |
| RUSTDESK_API_GORM_MAX_OPEN_CONNS | 数据库最大打开连接数 | 100 | | RUSTDESK_API_GORM_MAX_IDLE_CONNS | 数据库最大空闲连接数 | 10 |
| RUSTDESK_API_RUSTDESK_PERSONAL | 是否启用个人版API 1:启用,0:不启用; 默认启用 | 1 | | RUSTDESK_API_GORM_MAX_OPEN_CONNS | 数据库最大打开连接数 | 100 |
| -----MYSQL配置----- | ---------- | ---------- | | RUSTDESK_API_RUSTDESK_PERSONAL | 是否启用个人版API 1:启用,0:不启用; 默认启用 | 1 |
| RUSTDESK_API_MYSQL_USERNAME | mysql用户名 | root | | -----MYSQL配置----- | ---------- | ---------- |
| RUSTDESK_API_MYSQL_PASSWORD | mysql密码 | 111111 | | RUSTDESK_API_MYSQL_USERNAME | mysql用户名 | root |
| RUSTDESK_API_MYSQL_ADDR | mysql地址 | 192.168.1.66:3306 | | RUSTDESK_API_MYSQL_PASSWORD | mysql密码 | 111111 |
| RUSTDESK_API_MYSQL_DBNAME | mysql数据库名 | rustdesk | | RUSTDESK_API_MYSQL_ADDR | mysql地址 | 192.168.1.66:3306 |
| -----RUSTDESK配置----- | --------------- | ---------- | | RUSTDESK_API_MYSQL_DBNAME | mysql数据库名 | rustdesk |
| RUSTDESK_API_RUSTDESK_ID_SERVER | Rustdesk的id服务器地址 | 192.168.1.66:21116 | | -----RUSTDESK配置----- | --------------- | ---------- |
| RUSTDESK_API_RUSTDESK_RELAY_SERVER | Rustdesk的relay服务器地址 | 192.168.1.66:21117 | | RUSTDESK_API_RUSTDESK_ID_SERVER | Rustdesk的id服务器地址 | 192.168.1.66:21116 |
| RUSTDESK_API_RUSTDESK_API_SERVER | Rustdesk的api服务器地址 | http://192.168.1.66:21114 | | RUSTDESK_API_RUSTDESK_RELAY_SERVER | Rustdesk的relay服务器地址 | 192.168.1.66:21117 |
| RUSTDESK_API_RUSTDESK_KEY | Rustdesk的key | 123456789 | | RUSTDESK_API_RUSTDESK_API_SERVER | Rustdesk的api服务器地址 | http://192.168.1.66:21114 |
| RUSTDESK_API_RUSTDESK_KEY | Rustdesk的key | 123456789 |
| ----PROXY配置----- | --------------- | ---------- |
| RUSTDESK_API_PROXY_ENABLE | 是否启用代理:`false`, `true` | `false` |
| RUSTDESK_API_PROXY_HOST | 代理地址 | `http://127.0.0.1:1080` |
### 安装步骤
### 运行
#### docker运行 #### docker运行
1. 直接docker运行,配置可以通过挂载配置文件`/app/conf/config.yaml`来修改,或者通过环境变量覆盖配置文件中的配置 1. 直接docker运行,配置可以通过挂载配置文件`/app/conf/config.yaml`来修改,或者通过环境变量覆盖配置文件中的配置
```bash ```bash
docker run -d --name rustdesk-api -p 21114:21114 \ docker run -d --name rustdesk-api -p 21114:21114 \
-v /data/rustdesk/api:/app/data \ -v /data/rustdesk/api:/app/data \
-e TZ=Asia/Shanghai \ -e TZ=Asia/Shanghai \
-e RUSTDESK_API_RUSTDESK_ID_SERVER=192.168.1.66:21116 \ -e RUSTDESK_API_LANG=zh-CN \
-e RUSTDESK_API_RUSTDESK_RELAY_SERVER=192.168.1.66:21117 \ -e RUSTDESK_API_RUSTDESK_ID_SERVER=192.168.1.66:21116 \
-e RUSTDESK_API_RUSTDESK_API_SERVER=http://192.168.1.66:21114 \ -e RUSTDESK_API_RUSTDESK_RELAY_SERVER=192.168.1.66:21117 \
-e RUSTDESK_API_RUSTDESK_KEY=123456789 \ -e RUSTDESK_API_RUSTDESK_API_SERVER=http://192.168.1.66:21114 \
lejianwen/rustdesk-api -e RUSTDESK_API_RUSTDESK_KEY=<key> \
``` lejianwen/rustdesk-api
```
2. 使用`docker compose` 2. 使用`docker compose`
- 简单示例
- 简单示例 ```yaml
```docker-compose
services: services:
rustdesk-api: rustdesk-api:
container_name: rustdesk-api container_name: rustdesk-api
@@ -198,7 +241,7 @@ lejianwen/rustdesk-api
- RUSTDESK_API_RUSTDESK_ID_SERVER=192.168.1.66:21116 - RUSTDESK_API_RUSTDESK_ID_SERVER=192.168.1.66:21116
- RUSTDESK_API_RUSTDESK_RELAY_SERVER=192.168.1.66:21117 - RUSTDESK_API_RUSTDESK_RELAY_SERVER=192.168.1.66:21117
- RUSTDESK_API_RUSTDESK_API_SERVER=http://192.168.1.66:21114 - RUSTDESK_API_RUSTDESK_API_SERVER=http://192.168.1.66:21114
- RUSTDESK_API_RUSTDESK_KEY=123456789 - RUSTDESK_API_RUSTDESK_KEY=<key>
ports: ports:
- 21114:21114 - 21114:21114
image: lejianwen/rustdesk-api image: lejianwen/rustdesk-api
@@ -209,73 +252,70 @@ lejianwen/rustdesk-api
restart: unless-stopped restart: unless-stopped
``` ```
- 根据rustdesk提供的示例加上自己的rustdesk-api - 根据rustdesk官方提供的示例加上自己的rustdesk-api
- 如果是使用的系统生成的KEY去掉`-k <key>`参数,在启动后运行`docker-compose logs hbbs`或者`cat ./data/id_ed25519.pub`查看KEY然后再修改`RUSTDESK_API_RUSTDESK_KEY=<key>`再执行`docker-compose up -d`
```docker-compose ```yaml
networks: networks:
rustdesk-net: rustdesk-net:
external: false external: false
services: services:
hbbs: hbbs:
container_name: hbbs container_name: hbbs
ports: ports:
- 21115:21115 - 21115:21115
- 21116:21116 # 自定义 hbbs 映射端口 - 21116:21116 # 自定义 hbbs 映射端口
- 21116:21116/udp # 自定义 hbbs 映射端口 - 21116:21116/udp # 自定义 hbbs 映射端口
- 21118:21118 # web client 需要 - 21118:21118 # web client
image: rustdesk/rustdesk-server image: rustdesk/rustdesk-server
command: hbbs -r <relay-server-ip[:port]> -k 123456789 # 填入个人域名或 IP + hbbr 暴露端口 command: hbbs -r <relay-server-ip[:port]> -k <key> # 填入个人域名或 IP + hbbr 暴露端口
volumes: volumes:
- /data/rustdesk/hbbs:/root # 自定义挂载目录 - ./data:/root # 自定义挂载目录
networks: networks:
- rustdesk-net - rustdesk-net
depends_on: depends_on:
- hbbr - hbbr
restart: unless-stopped restart: unless-stopped
deploy: deploy:
resources: resources:
limits: limits:
memory: 64M memory: 64M
hbbr: hbbr:
container_name: hbbr container_name: hbbr
ports: ports:
- 21117:21117 # 自定义 hbbr 映射端口 - 21117:21117 # 自定义 hbbr 映射端口
image: rustdesk/rustdesk-server - 21119:21119 # web client
command: hbbr -k 123456789 image: rustdesk/rustdesk-server
#command: hbbr command: hbbr -k <key>
volumes: volumes:
- /data/rustdesk/hbbr:/root # 自定义挂载目录 - ./data:/root
networks: networks:
- rustdesk-net - rustdesk-net
restart: unless-stopped restart: unless-stopped
deploy: deploy:
resources: resources:
limits: limits:
memory: 64M memory: 64M
rustdesk-api: rustdesk-api:
container_name: rustdesk-api container_name: rustdesk-api
environment: environment:
- TZ=Asia/Shanghai - TZ=Asia/Shanghai
- RUSTDESK_API_RUSTDESK_ID_SERVER=192.168.1.66:21116 - RUSTDESK_API_RUSTDESK_ID_SERVER=192.168.1.66:21116
- RUSTDESK_API_RUSTDESK_RELAY_SERVER=192.168.1.66:21117 - RUSTDESK_API_RUSTDESK_RELAY_SERVER=192.168.1.66:21117
- RUSTDESK_API_RUSTDESK_API_SERVER=http://192.168.1.66:21114 - RUSTDESK_API_RUSTDESK_API_SERVER=http://192.168.1.66:21114
- RUSTDESK_API_RUSTDESK_KEY=123456789 - RUSTDESK_API_RUSTDESK_KEY=<key>
ports: ports:
- 21114:21114 - 21114:21114
image: lejianwen/rustdesk-api image: lejianwen/rustdesk-api
volumes: volumes:
- /data/rustdesk/api:/app/data #将数据库挂载出来方便备份 - /data/rustdesk/api:/app/data #将数据库挂载出来方便备份
networks: networks:
- rustdesk-net - rustdesk-net
restart: unless-stopped restart: unless-stopped
```
```
- S6的镜像
- 如果使用的是S6的镜像,会需要修改启动脚本,覆盖镜像中的`/etc/s6-overlay/s6-rc.d/hbbr/run` - 如果使用***自定义KEY***,会需要修改启动脚本,覆盖镜像中的`/etc/s6-overlay/s6-rc.d/hbbr/run`和`/etc/s6-overlay/s6-rc.d/hbbr/run`
和`/etc/s6-overlay/s6-rc.d/hbbr/run` 1. 创建`hbbr/run`自定义KEY才需要
1. 创建`hbbr/run`
```bash ```bash
#!/command/with-contenv sh #!/command/with-contenv sh
cd /data cd /data
@@ -283,63 +323,99 @@ lejianwen/rustdesk-api
[ "${ENCRYPTED_ONLY}" = "1" ] && PARAMS="-k ${KEY}" [ "${ENCRYPTED_ONLY}" = "1" ] && PARAMS="-k ${KEY}"
/usr/bin/hbbr $PARAMS /usr/bin/hbbr $PARAMS
``` ```
2. 创建`hbbs/run`自定义KEY才需要
2. 创建`hbbs/run` ```bash
```bash #!/command/with-contenv sh
#!/command/with-contenv sh sleep 2
sleep 2 cd /data
cd /data PARAMS=
PARAMS= [ "${ENCRYPTED_ONLY}" = "1" ] && PARAMS="-k ${KEY}"
[ "${ENCRYPTED_ONLY}" = "1" ] && PARAMS="-k ${KEY}" /usr/bin/hbbs -r $RELAY $PARAMS
/usr/bin/hbbs -r $RELAY $PARAMS ```
``` 3. 修改`docker-compose.yml`中的`s6`部分
3. 修改`docker-compose.yml`中的`s6`部分 ```yaml
networks:
``` rustdesk-net:
networks: external: false
rustdesk-net: services:
external: false rustdesk-server:
services: container_name: rustdesk-server
rustdesk-server: ports:
container_name: rustdesk-server - 21115:21115
ports: - 21116:21116
- 21115:21115 - 21116:21116/udp
- 21116:21116 - 21117:21117
- 21116:21116/udp - 21118:21118
- 21117:21117 - 21119:21119
- 21118:21118 image: rustdesk/rustdesk-server-s6:latest
- 21119:21119 environment:
image: rustdesk/rustdesk-server-s6:latest - RELAY=192.168.1.66:21117
environment: - ENCRYPTED_ONLY=1
- RELAY=192.168.1.66:21117 - KEY=<key> #自定义KEY
- ENCRYPTED_ONLY=1 volumes:
- KEY=abc123456789 - ./data:/data
volumes: - ./hbbr/run:/etc/s6-overlay/s6-rc.d/hbbr/run
- ./data:/data - ./hbbs/run:/etc/s6-overlay/s6-rc.d/hbbs/run
- ./hbbr/run:/etc/s6-overlay/s6-rc.d/hbbr/run restart: unless-stopped
- ./hbbs/run:/etc/s6-overlay/s6-rc.d/hbbs/run rustdesk-api:
restart: unless-stopped container_name: rustdesk-api
rustdesk-api: ports:
container_name: rustdesk-api - 21114:21114
ports: image: lejianwen/rustdesk-api
- 21114:21114 environment:
image: lejianwen/rustdesk-api - TZ=Asia/Shanghai
environment: - RUSTDESK_API_RUSTDESK_ID_SERVER=192.168.1.66:21116
- TZ=Asia/Shanghai - RUSTDESK_API_RUSTDESK_RELAY_SERVER=192.168.1.66:21117
- RUSTDESK_API_RUSTDESK_ID_SERVER=192.168.1.66:21116 - RUSTDESK_API_RUSTDESK_API_SERVER=http://192.168.1.66:21114
- RUSTDESK_API_RUSTDESK_RELAY_SERVER=192.168.1.66:21117 - RUSTDESK_API_RUSTDESK_KEY=<key>
- RUSTDESK_API_RUSTDESK_API_SERVER=http://192.168.1.66:21114 volumes:
- RUSTDESK_API_RUSTDESK_KEY=abc123456789 - /data/rustdesk/api:/app/data #将数据库挂载
volumes: networks:
- /data/rustdesk/api:/app/data #将数据库挂载 - rustdesk-net
networks: restart: unless-stopped
- rustdesk-net ```
restart: unless-stopped - 如果使用***系统生成的KEY***或者***自定义KEY_PUB,KEY_PRIV***不需要修改启动脚本但要在生成KEY后获取到KEY再`docker-compose up -d`
``` ```yaml
networks:
rustdesk-net:
external: false
services:
rustdesk-server:
container_name: rustdesk-server
ports:
- 21115:21115
- 21116:21116
- 21116:21116/udp
- 21117:21117
- 21118:21118
- 21119:21119
image: rustdesk/rustdesk-server-s6:latest
environment:
- RELAY=192.168.1.66:21117
- ENCRYPTED_ONLY=1
volumes:
- ./data:/data
restart: unless-stopped
rustdesk-api:
container_name: rustdesk-api
ports:
- 21114:21114
image: lejianwen/rustdesk-api
environment:
- TZ=Asia/Shanghai
- RUSTDESK_API_RUSTDESK_ID_SERVER=192.168.1.66:21116
- RUSTDESK_API_RUSTDESK_RELAY_SERVER=192.168.1.66:21117
- RUSTDESK_API_RUSTDESK_API_SERVER=http://192.168.1.66:21114
- RUSTDESK_API_RUSTDESK_KEY=<key> #系统生成的KEY
volumes:
- /data/rustdesk/api:/app/data #将数据库挂载
networks:
- rustdesk-net
restart: unless-stopped
```
#### 下载release直接运行 #### 下载release直接运行
下载地址[release](https://github.com/lejianwen/rustdesk-api/releases) [下载地址](https://github.com/lejianwen/rustdesk-api/releases)
#### 源码安装 #### 源码安装
@@ -379,7 +455,22 @@ lejianwen/rustdesk-api
6. 打开浏览器访问`http://<your server[:port]>/_admin/`,默认用户名密码为`admin`,请及时更改密码。 6. 打开浏览器访问`http://<your server[:port]>/_admin/`,默认用户名密码为`admin`,请及时更改密码。
#### nginx反代
在`nginx`中配置反代
```
server {
listen <your port>;
server_name <your server>;
location / {
proxy_pass http://<api-server[:port]>;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
```
## 其他 ## 其他
- [修改客户端ID](https://github.com/abdullah-erturk/RustDesk-ID-Changer) - [修改客户端ID](https://github.com/abdullah-erturk/RustDesk-ID-Changer)
- [webclient](https://hub.docker.com/r/keyurbhole/flutter_web_desk) - [webclient来源](https://hub.docker.com/r/keyurbhole/flutter_web_desk)

View File

@@ -8,8 +8,7 @@ desktop software that provides self-hosted solutions.
<img src="https://img.shields.io/badge/gin-v1.9.0-lightBlue"/> <img src="https://img.shields.io/badge/gin-v1.9.0-lightBlue"/>
<img src="https://img.shields.io/badge/gorm-v1.25.7-green"/> <img src="https://img.shields.io/badge/gorm-v1.25.7-green"/>
<img src="https://img.shields.io/badge/swag-v1.16.3-yellow"/> <img src="https://img.shields.io/badge/swag-v1.16.3-yellow"/>
<img src="https://github.com/lejianwen/rustdesk-api/actions/workflows/release.yml/badge.svg"/> <img src="https://github.com/lejianwen/rustdesk-api/actions/workflows/build.yml/badge.svg"/>
<img src="https://github.com/lejianwen/rustdesk-api/actions/workflows/docker.yml/badge.svg"/>
</div> </div>
# Features # Features
@@ -30,34 +29,64 @@ desktop software that provides self-hosted solutions.
- OAuth Management - OAuth Management
- Quick access to web client - Quick access to web client
- i18n - i18n
- Share to guest by web client
- Web Client - Web Client
- Automatically obtain API server - Automatically obtain API server
- Automatically obtain ID server and KEY - Automatically obtain ID server and KEY
- Automatically obtain address book - Automatically obtain address book
- Visitors are remotely to the device via a temporary sharing link
## Prerequisites ## Prerequisites
### [Rustdesk](https://github.com/rustdesk/rustdesk) ### [Rustdesk](https://github.com/rustdesk/rustdesk)
1. The PC client version used is ***1.3.0***, and versions ***1.2.6+*** have been tested to work.
2. The server must specify a key, and not use the auto-generated key, otherwise there may be connection failures or
timeouts.
```bash #### The PC client uses version ***1.3.0***, and versions ***1.2.6+*** have been tested to work.
hbbs -r <relay-server-ip[:port]> -k <key>
hbbr -k <key>
```
Example: #### Solutions for PC client connection timeout or connection issues
##### Connection issues or timeouts
Because the server version lags behind the client version, the server does not respond to the client's `secure_tcp` request, causing the client to timeout.
Relevant code can be found at `https://github.com/rustdesk/rustdesk/blob/master/src/client.rs#L322`
```rust
if !key.is_empty() && !token.is_empty() {
// mainly for the security of token
allow_err!(secure_tcp(&mut socket, key).await);
}
```
As seen, when both `key` and `token` are not empty, `secure_tcp` is called, but the server does not respond, causing the client to timeout.
The `secure_tcp` code is located at `https://github.com/rustdesk/rustdesk/blob/master/src/common.rs#L1203`
```bash ##### Four Solutions
hbbs -r <relay-server-ip[:port]> -k abc1234567 1. Specify the key on the server.
hbbr -k abc1234567 - Advantage: Simple
``` - Disadvantage: The connection is not encrypted
```bash
hbbs -r <relay-server-ip[:port]> -k <key>
hbbr -k <key>
```
For example
```bash
hbbs -r <relay-server-ip[:port]> -k abc1234567
hbbr -k abc1234567
```
2. Use a system-generated key or a custom key pair on the server. If the client is already logged in, it may timeout or fail to connect. Logging out and reconnecting usually resolves the issue, and the web client does not need to log out.
- Advantage: Encrypted connection
- Disadvantage: Complicated operation
3. Use a system-generated key or a custom key pair on the server, fork the official client code to modify `secure_tcp` to return directly, then compile using `Github Actions` and download the compiled client.
Refer to [official documentation](https://rustdesk.com/docs/en/dev/build/all/)
- Advantage: Encrypted connection, customizable client features, ready to use after compilation
- Disadvantage: Requires forking code and compiling, which can be challenging
4. Use [my forked code](https://github.com/lejianwen/rustdesk), which has already modified `secure_tcp`. You can download and use it directly from [here](https://github.com/lejianwen/rustdesk/releases)
- Advantage: Code changes are viewable, compiled with `Github Actions`, encrypted connection, ready to use
- Disadvantage: May not keep up with official version updates
***If encryption is not a high priority, use `1`. If encryption is important, use `3` or `4`.***
## Overview ## Overview
### API Service: Basic implementation of the PC client's primary interfaces.Supports the Personal version api, which can be enabled by configuring the `rustdesk.personal` file or the `RUSTDESK_API_RUSTDESK_PERSONAL` environment variable. ### API Service
Basic implementation of the PC client's primary interfaces.Supports the Personal version api, which can be enabled by configuring the `rustdesk.personal` file or the `RUSTDESK_API_RUSTDESK_PERSONAL` environment variable.
#### Login #### Login
@@ -65,38 +94,39 @@ desktop software that provides self-hosted solutions.
configuration section for details. configuration section for details.
- Added authorization login for the web admin panel. - Added authorization login for the web admin panel.
![pc_login](docs/pc_login.png) ![pc_login](docs/en_img/pc_login.png)
#### Address Book #### Address Book
![pc_ab](docs/pc_ab.png) ![pc_ab](docs/en_img/pc_ab.png)
#### Groups: Groups are divided into `shared groups` and `regular groups`. In shared groups, everyone can see the peers of all group members, while in regular groups, only administrators can see all members' peers. #### Groups
Groups are divided into `shared groups` and `regular groups`. In shared groups, everyone can see the peers of all group members, while in regular groups, only administrators can see all members' peers.
![pc_gr](docs/pc_gr.png) ![pc_gr](docs/en_img/pc_gr.png)
### Web Admin ### Web Admin
***The frontend and backend are separated to provide a user-friendly management interface, primarily for managing and * The frontend and backend are separated to provide a user-friendly management interface, primarily for managing and
displaying data.Frontend code is available at [rustdesk-api-web](https://github.com/lejianwen/rustdesk-api-web)*** displaying data.Frontend code is available at [rustdesk-api-web](https://github.com/lejianwen/rustdesk-api-web)
***Admin panel URL: `http://<your server[:port]>/_admin/`. The default username and password for the initial * Admin panel URL: `http://<your server[:port]>/_admin/`. The default username and password for the initial
installation are `admin` `admin`, please change the password immediately.*** installation are `admin` `admin`, please change the password immediately.
1. Admin interface: 1. Admin interface:
![web_admin](docs/web_admin.png) ![web_admin](docs/en_img/web_admin.png)
2. Regular user interface: 2. Regular user interface:
![web_user](docs/web_admin_user.png) ![web_user](docs/en_img/web_admin_user.png)
You can change your password from the top right corner: You can change your password from the top right corner:
![web_resetpwd](docs/web_resetpwd.png) ![web_resetpwd](docs/en_img/web_resetpwd.png)
3. Groups can be customized for easy management. Currently, two types are supported: `shared group` and `regular group`. 3. Groups can be customized for easy management. Currently, two types are supported: `shared group` and `regular group`.
![web_admin_gr](docs/web_admin_gr.png) ![web_admin_gr](docs/en_img/web_admin_gr.png)
4. You can open the web client directly for convenience: 4. You can directly open the web client for convenient use; it can also be shared with guests, allowing them to remotely access the device via the web client.
![web_webclient](docs/admin_webclient.png) ![web_webclient](docs/en_img/admin_webclient.png)
5. OAuth support: Currently, `GitHub` and `Google` is supported. You need to create an `OAuth App` and configure it in 5. OAuth support: Currently, `GitHub` and `Google` is supported. You need to create an `OAuth App` and configure it in
the admin the admin
panel. panel.
![web_admin_oauth](docs/web_admin_oauth.png) ![web_admin_oauth](docs/en_img/web_admin_oauth.png)
- Create a `GitHub OAuth App` - Create a `GitHub OAuth App`
at `Settings` -> `Developer settings` -> `OAuth Apps` -> `New OAuth App` [here](https://github.com/settings/developers). at `Settings` -> `Developer settings` -> `OAuth Apps` -> `New OAuth App` [here](https://github.com/settings/developers).
- Set the `Authorization callback URL` to `http://<your server[:port]>/api/oauth/callback`, - Set the `Authorization callback URL` to `http://<your server[:port]>/api/oauth/callback`,
@@ -121,11 +151,14 @@ installation are `admin` `admin`, please change the password immediately.***
### Configuration ### Configuration
* Modify the configuration in `conf/config.yaml`. If `gorm.type` is set to `sqlite`, MySQL-related configurations are * Modify the configuration in `conf/config.yaml`.
not required. * If `gorm.type` is set to `sqlite`, MySQL-related configurations are not required.
* Language support: `en` and `zh-CN` are supported. The default is `zh-CN`.
```yaml ```yaml
lang: "en" lang: "en"
app:
web-client: 1 # web client route 1:open 0:close
gin: gin:
api-addr: "0.0.0.0:21114" api-addr: "0.0.0.0:21114"
mode: "release" mode: "release"
@@ -146,33 +179,40 @@ rustdesk:
api-server: "http://192.168.1.66:21114" api-server: "http://192.168.1.66:21114"
key: "123456789" key: "123456789"
personal: 1 personal: 1
logger:
path: "./runtime/log.txt"
level: "warn" #trace,debug,info,warn,error,fatal
report-caller: true
``` ```
* Environment variables, with the prefix `RUSTDESK_API_RUSTDESK_PERSONAL`, will override the settings in the ### Environment Variables
configuration file if The prefix for variable names is `RUSTDESK_API`. If environment variables exist, they will override the configurations in the configuration file.
present.
| Variable Name | Description | Example | | Variable Name | Description | Example |
|------------------------------------|-----------------------------------------------------------|--------------------------------| |------------------------------------|-----------------------------------------------------------|-------------------------------|
| TZ | timezone | Asia/Shanghai | | TZ | timezone | Asia/Shanghai |
| RUSTDESK_API_LANG | Language | `en`,`zh-CN` | | RUSTDESK_API_LANG | Language | `en`,`zh-CN` |
| ----- GIN Configuration ----- | --------------------------------------- | ------------------------------ | | RUSTDESK_API_APP_WEB_CLIENT | web client on/off; 1: on, 0 off, deault 1 | 1 |
| RUSTDESK_API_GIN_TRUST_PROXY | Trusted proxy IPs, separated by commas. | 192.168.1.2,192.168.1.3 | | ----- GIN Configuration ----- | --------------------------------------- | ----------------------------- |
| ----- GORM Configuration ----- | --------------------------------------- | ------------------------------ | | RUSTDESK_API_GIN_TRUST_PROXY | Trusted proxy IPs, separated by commas. | 192.168.1.2,192.168.1.3 |
| RUSTDESK_API_GORM_TYPE | Database type (`sqlite` or `mysql`). Default is `sqlite`. | sqlite | | ----- GORM Configuration ----- | --------------------------------------- | ----------------------------- |
| RUSTDESK_API_GORM_MAX_IDLE_CONNS | Maximum idle connections | 10 | | RUSTDESK_API_GORM_TYPE | Database type (`sqlite` or `mysql`). Default is `sqlite`. | sqlite |
| RUSTDESK_API_GORM_MAX_OPEN_CONNS | Maximum open connections | 100 | | RUSTDESK_API_GORM_MAX_IDLE_CONNS | Maximum idle connections | 10 |
| RUSTDESK_API_RUSTDESK_PERSONAL | Open Personal Api 1:Enable,0:Disable | 1 | | RUSTDESK_API_GORM_MAX_OPEN_CONNS | Maximum open connections | 100 |
| ----- MYSQL Configuration ----- | --------------------------------------- | ------------------------------ | | RUSTDESK_API_RUSTDESK_PERSONAL | Open Personal Api 1:Enable,0:Disable | 1 |
| RUSTDESK_API_MYSQL_USERNAME | MySQL username | root | | ----- MYSQL Configuration ----- | --------------------------------------- | ----------------------------- |
| RUSTDESK_API_MYSQL_PASSWORD | MySQL password | 111111 | | RUSTDESK_API_MYSQL_USERNAME | MySQL username | root |
| RUSTDESK_API_MYSQL_ADDR | MySQL address | 192.168.1.66:3306 | | RUSTDESK_API_MYSQL_PASSWORD | MySQL password | 111111 |
| RUSTDESK_API_MYSQL_DBNAME | MySQL database name | rustdesk | | RUSTDESK_API_MYSQL_ADDR | MySQL address | 192.168.1.66:3306 |
| ----- RUSTDESK Configuration ----- | --------------------------------------- | ------------------------------ | | RUSTDESK_API_MYSQL_DBNAME | MySQL database name | rustdesk |
| RUSTDESK_API_RUSTDESK_ID_SERVER | Rustdesk ID server address | 192.168.1.66:21116 | | ----- RUSTDESK Configuration ----- | --------------------------------------- | ----------------------------- |
| RUSTDESK_API_RUSTDESK_RELAY_SERVER | Rustdesk relay server address | 192.168.1.66:21117 | | RUSTDESK_API_RUSTDESK_ID_SERVER | Rustdesk ID server address | 192.168.1.66:21116 |
| RUSTDESK_API_RUSTDESK_API_SERVER | Rustdesk API server address | http://192.168.1.66:21114 | | RUSTDESK_API_RUSTDESK_RELAY_SERVER | Rustdesk relay server address | 192.168.1.66:21117 |
| RUSTDESK_API_RUSTDESK_KEY | Rustdesk key | 123456789 | | RUSTDESK_API_RUSTDESK_API_SERVER | Rustdesk API server address | http://192.168.1.66:21114 |
| RUSTDESK_API_RUSTDESK_KEY | Rustdesk key | 123456789 |
| ---- PROXY ----- | --------------- | ---------- |
| RUSTDESK_API_PROXY_ENABLE | proxy_enable :`false`, `true` | `false` |
| RUSTDESK_API_PROXY_HOST | proxy_host | `http://127.0.0.1:1080` |
### Installation Steps ### Installation Steps
@@ -180,164 +220,201 @@ rustdesk:
1. Run directly with Docker. Configuration can be modified by mounting the config file `/app/conf/config.yaml`, or by 1. Run directly with Docker. Configuration can be modified by mounting the config file `/app/conf/config.yaml`, or by
using environment variables to override settings. using environment variables to override settings.
```bash ```bash
docker run -d --name rustdesk-api -p 21114:21114 \ docker run -d --name rustdesk-api -p 21114:21114 \
-v /data/rustdesk/api:/app/data \ -v /data/rustdesk/api:/app/data \
-e RUSTDESK_API_RUSTDESK_ID_SERVER=192.168.1.66:21116 \ -e RUSTDESK_API_LANG=en \
-e RUSTDESK_API_RUSTDESK_RELAY_SERVER=192.168.1.66:21117 \ -e RUSTDESK_API_RUSTDESK_ID_SERVER=192.168.1.66:21116 \
-e RUSTDESK_API_RUSTDESK_API_SERVER=http://192.168.1.66:21114 \ -e RUSTDESK_API_RUSTDESK_RELAY_SERVER=192.168.1.66:21117 \
-e RUSTDESK_API_RUSTDESK_KEY=123456789 \ -e RUSTDESK_API_RUSTDESK_API_SERVER=http://192.168.1.66:21114 \
lejianwen/rustdesk-api -e RUSTDESK_API_RUSTDESK_KEY=abc123456 \
``` lejianwen/rustdesk-api
```
2. Using `docker-compose` 2. Using `docker-compose`
- Simple example: - Simple example:
```yaml
```docker-compose services:
services: rustdesk-api:
rustdesk-api: container_name: rustdesk-api
container_name: rustdesk-api environment:
environment: - RUSTDESK_API_LANG=en
- RUSTDESK_API_RUSTDESK_ID_SERVER=192.168.1.66:21116 - RUSTDESK_API_RUSTDESK_ID_SERVER=192.168.1.66:21116
- RUSTDESK_API_RUSTDESK_RELAY_SERVER=192.168.1.66:21117 - RUSTDESK_API_RUSTDESK_RELAY_SERVER=192.168.1.66:21117
- RUSTDESK_API_RUSTDESK_API_SERVER=http://192.168.1.66:21114 - RUSTDESK_API_RUSTDESK_API_SERVER=http://192.168.1.66:21114
- RUSTDESK_API_RUSTDESK_KEY=123456789 - RUSTDESK_API_RUSTDESK_KEY=<key>
ports: ports:
- 21114:21114 - 21114:21114
image: lejianwen/rustdesk-api image: lejianwen/rustdesk-api
volumes: volumes:
- /data/rustdesk/api:/app/data # Mount the database for easy backup - /data/rustdesk/api:/app/data # Mount the database for easy backup
networks: networks:
- rustdesk-net - rustdesk-net
restart: unless-stopped restart: unless-stopped
``` ```
- Example with RustDesk's official Docker Compose file, adding your `rustdesk-api` service: - Example with RustDesk's official Docker Compose file, adding your `rustdesk-api` service:
- If you are using a system-generated KEY, remove the `-k <key>` parameter. However, after the first startup, run `docker-compose logs hbbs` or `cat ./data/id_ed25519.pub` to view the KEY, then modify `RUSTDESK_API_RUSTDESK_KEY=<key>` and execute `docker-compose up -d` again.
```yaml
networks:
rustdesk-net:
external: false
services:
hbbs:
container_name: hbbs
ports:
- 21115:21115
- 21116:21116 # 自定义 hbbs 映射端口
- 21116:21116/udp # 自定义 hbbs 映射端口
- 21118:21118 # web client
image: rustdesk/rustdesk-server
command: hbbs -r <relay-server-ip[:port]> -k <key> # 填入个人域名或 IP + hbbr 暴露端口
volumes:
- ./data:/root # 自定义挂载目录
networks:
- rustdesk-net
depends_on:
- hbbr
restart: unless-stopped
deploy:
resources:
limits:
memory: 64M
hbbr:
container_name: hbbr
ports:
- 21117:21117 # 自定义 hbbr 映射端口
- 21119:21119 # web client
image: rustdesk/rustdesk-server
command: hbbr -k <key>
volumes:
- ./data:/root
networks:
- rustdesk-net
restart: unless-stopped
deploy:
resources:
limits:
memory: 64M
rustdesk-api:
container_name: rustdesk-api
environment:
- TZ=Asia/Shanghai
- RUSTDESK_API_RUSTDESK_ID_SERVER=192.168.1.66:21116
- RUSTDESK_API_RUSTDESK_RELAY_SERVER=192.168.1.66:21117
- RUSTDESK_API_RUSTDESK_API_SERVER=http://192.168.1.66:21114
- RUSTDESK_API_RUSTDESK_KEY=<key>
ports:
- 21114:21114
image: lejianwen/rustdesk-api
volumes:
- /data/rustdesk/api:/app/data #将数据库挂载出来方便备份
networks:
- rustdesk-net
restart: unless-stopped
```
```docker-compose - S6 image
networks: - - If using ***custom KEY***, you will need to modify the startup script to override the `/etc/s6-overlay/s6-rc.d/hbbr/run` and `/etc/s6-overlay/s6-rc.d/hbbr/run` in the image.
rustdesk-net: 1. Create `hbbr/run`, only needed for custom KEY
external: false ```bash
services: #!/command/with-contenv sh
hbbs: cd /data
container_name: hbbs PARAMS=
ports: [ "${ENCRYPTED_ONLY}" = "1" ] && PARAMS="-k ${KEY}"
- 21115:21115 /usr/bin/hbbr $PARAMS
- 21116:21116 # 自定义 hbbs 映射端口 ```
- 21116:21116/udp # 自定义 hbbs 映射端口 2. Create `hbbs/run`, only needed for custom KEY
- 21118:21118 # web client 需要 ```bash
image: rustdesk/rustdesk-server #!/command/with-contenv sh
command: hbbs -r <relay-server-ip[:port]> -k 123456789 # 填入个人域名或 IP + hbbr 暴露端口 sleep 2
volumes: cd /data
- /data/rustdesk/hbbs:/root # 自定义挂载目录 PARAMS=
[ "${ENCRYPTED_ONLY}" = "1" ] && PARAMS="-k ${KEY}"
/usr/bin/hbbs -r $RELAY $PARAMS
```
3. Modify the `s6` section in `docker-compose.yml`
```yaml
networks:
rustdesk-net:
external: false
services:
rustdesk-server:
container_name: rustdesk-server
ports:
- 21115:21115
- 21116:21116
- 21116:21116/udp
- 21117:21117
- 21118:21118
- 21119:21119
image: rustdesk/rustdesk-server-s6:latest
environment:
- RELAY=192.168.1.66:21117
- ENCRYPTED_ONLY=1
- KEY=<key> #KEY
volumes:
- ./data:/data
- ./hbbr/run:/etc/s6-overlay/s6-rc.d/hbbr/run
- ./hbbs/run:/etc/s6-overlay/s6-rc.d/hbbs/run
restart: unless-stopped
rustdesk-api:
container_name: rustdesk-api
ports:
- 21114:21114
image: lejianwen/rustdesk-api
environment:
- TZ=Asia/Shanghai
- RUSTDESK_API_RUSTDESK_ID_SERVER=192.168.1.66:21116
- RUSTDESK_API_RUSTDESK_RELAY_SERVER=192.168.1.66:21117
- RUSTDESK_API_RUSTDESK_API_SERVER=http://192.168.1.66:21114
- RUSTDESK_API_RUSTDESK_KEY=<key>
volumes:
- /data/rustdesk/api:/app/data
networks:
- rustdesk-net
restart: unless-stopped
```
- If using ***system-generated KEY*** or ***custom KEY_PUB, KEY_PRIV***, you do not need to modify the startup script, but you need to obtain the KEY after it is generated and then run `docker-compose up -d`
```yaml
networks: networks:
- rustdesk-net rustdesk-net:
depends_on: external: false
- hbbr services:
restart: unless-stopped rustdesk-server:
deploy: container_name: rustdesk-server
resources: ports:
limits: - 21115:21115
memory: 64M - 21116:21116
hbbr: - 21116:21116/udp
container_name: hbbr - 21117:21117
ports: - 21118:21118
- 21117:21117 # 自定义 hbbr 映射端口 - 21119:21119
image: rustdesk/rustdesk-server image: rustdesk/rustdesk-server-s6:latest
command: hbbr -k 123456789 environment:
#command: hbbr - RELAY=192.168.1.66:21117
volumes: - ENCRYPTED_ONLY=1
- /data/rustdesk/hbbr:/root # 自定义挂载目录 volumes:
networks: - ./data:/data
- rustdesk-net restart: unless-stopped
restart: unless-stopped rustdesk-api:
deploy: container_name: rustdesk-api
resources: ports:
limits: - 21114:21114
memory: 64M image: lejianwen/rustdesk-api
rustdesk-api: environment:
container_name: rustdesk-api - TZ=Asia/Shanghai
environment: - RUSTDESK_API_RUSTDESK_ID_SERVER=192.168.1.66:21116
- RUSTDESK_API_RUSTDESK_ID_SERVER=192.168.1.66:21116 - RUSTDESK_API_RUSTDESK_RELAY_SERVER=192.168.1.66:21117
- RUSTDESK_API_RUSTDESK_RELAY_SERVER=192.168.1.66:21117 - RUSTDESK_API_RUSTDESK_API_SERVER=http://192.168.1.66:21114
- RUSTDESK_API_RUSTDESK_API_SERVER=http://192.168.1.66:21114 - RUSTDESK_API_RUSTDESK_KEY=<key>
- RUSTDESK_API_RUSTDESK_KEY=123456789 volumes:
ports: - /data/rustdesk/api:/app/data
- 21114:21114 networks:
image: lejianwen/rustdesk-api - rustdesk-net
volumes: restart: unless-stopped
- /data/rustdesk/api:/app/data #将数据库挂载出来方便备份 ```
networks:
- rustdesk-net
restart: unless-stopped
```
- If you are using an S6 image, you need to modify the startup script `/etc/s6-overlay/s6-rc.d/hbbr/run`
and `/etc/s6-overlay/s6-rc.d/hbbr/run`
1. create `hbbr/run`
```bash
#!/command/with-contenv sh
cd /data
PARAMS=
[ "${ENCRYPTED_ONLY}" = "1" ] && PARAMS="-k ${KEY}"
/usr/bin/hbbr $PARAMS
```
2. create `hbbs/run`
```bash
#!/command/with-contenv sh
sleep 2
cd /data
PARAMS=
[ "${ENCRYPTED_ONLY}" = "1" ] && PARAMS="-k ${KEY}"
/usr/bin/hbbs -r $RELAY $PARAMS
```
3. edit `docker-compose.yml`
```
networks:
rustdesk-net:
external: false
services:
rustdesk-server:
container_name: rustdesk-server
ports:
- 21115:21115
- 21116:21116
- 21116:21116/udp
- 21117:21117
- 21118:21118
- 21119:21119
image: rustdesk/rustdesk-server-s6:latest
environment:
- RELAY=192.168.1.66:21117
- ENCRYPTED_ONLY=1
- KEY=abc123456789
volumes:
- ./data:/data
- ./hbbr/run:/etc/s6-overlay/s6-rc.d/hbbr/run
- ./hbbs/run:/etc/s6-overlay/s6-rc.d/hbbs/run
restart: unless-stopped
rustdesk-api:
container_name: rustdesk-api
ports:
- 21114:21114
image: lejianwen/rustdesk-api
environment:
- TZ=Asia/Shanghai
- RUSTDESK_API_RUSTDESK_ID_SERVER=192.168.1.66:21116
- RUSTDESK_API_RUSTDESK_RELAY_SERVER=192.168.1.66:21117
- RUSTDESK_API_RUSTDESK_API_SERVER=http://192.168.1.66:21114
- RUSTDESK_API_RUSTDESK_KEY=abc123456789
volumes:
- /data/rustdesk/api:/app/data #将数据库挂载
networks:
- rustdesk-net
restart: unless-stopped
```
#### Running from Release #### Running from Release
@@ -386,7 +463,22 @@ Download the release from [release](https://github.com/lejianwen/rustdesk-api/re
6. Open your browser and visit `http://<your server[:port]>/_admin/`, with default credentials `admin admin`. Please 6. Open your browser and visit `http://<your server[:port]>/_admin/`, with default credentials `admin admin`. Please
change the password promptly. change the password promptly.
## Miscellaneous #### nginx reverse proxy
Configure reverse proxy in `nginx`
```
server {
listen <your port>;
server_name <your server>;
location / {
proxy_pass http://<api-server[:port]>;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
```
## Others
- [Change client ID](https://github.com/abdullah-erturk/RustDesk-ID-Changer) - [Change client ID](https://github.com/abdullah-erturk/RustDesk-ID-Changer)
- [webclient](https://hub.docker.com/r/keyurbhole/flutter_web_desk) - [Web client source](https://hub.docker.com/r/keyurbhole/flutter_web_desk)

View File

@@ -23,6 +23,7 @@ import (
"github.com/go-redis/redis/v8" "github.com/go-redis/redis/v8"
"github.com/nicksnyder/go-i18n/v2/i18n" "github.com/nicksnyder/go-i18n/v2/i18n"
"golang.org/x/text/language" "golang.org/x/text/language"
nethttp "net/http"
"reflect" "reflect"
) )
@@ -47,6 +48,8 @@ func main() {
ReportCaller: global.Config.Logger.ReportCaller, ReportCaller: global.Config.Logger.ReportCaller,
}) })
InitI18n()
//redis //redis
global.Redis = redis.NewClient(&redis.Options{ global.Redis = redis.NewClient(&redis.Options{
Addr: global.Config.Redis.Addr, Addr: global.Config.Redis.Addr,
@@ -103,7 +106,6 @@ func main() {
//locker //locker
global.Lock = lock.NewLocal() global.Lock = lock.NewLocal()
InitI18n()
//gin //gin
http.ApiInit() http.ApiInit()
@@ -198,7 +200,7 @@ func getTranslatorForLang(lang string) ut.Translator {
} }
} }
func DatabaseAutoUpdate() { func DatabaseAutoUpdate() {
version := 126 version := 233
db := global.DB db := global.DB
@@ -259,6 +261,8 @@ func Migrate(version uint) {
&model.UserThird{}, &model.UserThird{},
&model.Oauth{}, &model.Oauth{},
&model.LoginLog{}, &model.LoginLog{},
&model.ShareRecord{},
&model.AuditConn{},
) )
if err != nil { if err != nil {
fmt.Println("migrate err :=>", err) fmt.Println("migrate err :=>", err)
@@ -268,13 +272,23 @@ func Migrate(version uint) {
var vc int64 var vc int64
global.DB.Model(&model.Version{}).Count(&vc) global.DB.Model(&model.Version{}).Count(&vc)
if vc == 1 { if vc == 1 {
localizer := global.Localizer(&gin.Context{
Request: &nethttp.Request{},
})
defaultGroup, _ := localizer.LocalizeMessage(&i18n.Message{
ID: "DefaultGroup",
})
group := &model.Group{ group := &model.Group{
Name: "默认组", Name: defaultGroup,
Type: model.GroupTypeDefault, Type: model.GroupTypeDefault,
} }
service.AllService.GroupService.Create(group) service.AllService.GroupService.Create(group)
shareGroup, _ := localizer.LocalizeMessage(&i18n.Message{
ID: "ShareGroup",
})
groupShare := &model.Group{ groupShare := &model.Group{
Name: "共享组", Name: shareGroup,
Type: model.GroupTypeShare, Type: model.GroupTypeShare,
} }
service.AllService.GroupService.Create(groupShare) service.AllService.GroupService.Create(groupShare)
@@ -282,7 +296,7 @@ func Migrate(version uint) {
is_admin := true is_admin := true
admin := &model.User{ admin := &model.User{
Username: "admin", Username: "admin",
Nickname: "管理员", Nickname: "Admin",
Status: model.COMMON_STATUS_ENABLE, Status: model.COMMON_STATUS_ENABLE,
IsAdmin: &is_admin, IsAdmin: &is_admin,
GroupId: 1, GroupId: 1,

View File

@@ -1,4 +1,6 @@
lang: "zh-CN" lang: "zh-CN"
app:
web-client: 1 # 1:启用 0:禁用
gin: gin:
api-addr: "0.0.0.0:21114" api-addr: "0.0.0.0:21114"
mode: "release" #release,debug,test mode: "release" #release,debug,test
@@ -16,13 +18,16 @@ mysql:
rustdesk: rustdesk:
id-server: "192.168.1.66:21116" id-server: "192.168.1.66:21116"
relay-server: "192.168.1.66:21117" relay-server: "192.168.1.66:21117"
api-server: "http://192.168.1.66:21114" api-server: "http://127.0.0.1:21114"
key: "123456789" key: "123456789"
personal: 1 personal: 1
logger: logger:
path: "./runtime/log.txt" path: "./runtime/log.txt"
level: "warn" #trace,debug,info,warn,error,fatal level: "warn" #trace,debug,info,warn,error,fatal
report-caller: true report-caller: true
proxy:
enable: false
host: ""
redis: redis:
addr: "127.0.0.1:6379" addr: "127.0.0.1:6379"
password: "" password: ""
@@ -42,4 +47,4 @@ oss:
max-byte: 10240 max-byte: 10240
jwt: jwt:
private-key: "./conf/jwt_pri.pem" private-key: "./conf/jwt_pri.pem"
expire-duration: 360000 expire-duration: 360000

View File

@@ -14,8 +14,13 @@ const (
DefaultConfig = "conf/config.yaml" DefaultConfig = "conf/config.yaml"
) )
type App struct {
WebClient int `mapstructure:"web-client"`
}
type Config struct { type Config struct {
Lang string `mapstructure:"lang"` Lang string `mapstructure:"lang"`
App App
Gorm Gorm Gorm Gorm
Mysql Mysql Mysql Mysql
Gin Gin Gin Gin
@@ -25,6 +30,7 @@ type Config struct {
Oss Oss Oss Oss
Jwt Jwt Jwt Jwt
Rustdesk Rustdesk Rustdesk Rustdesk
Proxy Proxy
} }
// Init 初始化配置 // Init 初始化配置

6
config/proxy.go Normal file
View File

@@ -0,0 +1,6 @@
package config
type Proxy struct {
Enable bool `mapstructure:"enable"`
Host string `mapstructure:"host"`
}

View File

@@ -22,7 +22,7 @@ const docTemplateadmin = `{
"token": [] "token": []
} }
], ],
"description": "创建地址簿", "description": "批量创建地址簿",
"consumes": [ "consumes": [
"application/json" "application/json"
], ],
@@ -32,7 +32,7 @@ const docTemplateadmin = `{
"tags": [ "tags": [
"地址簿" "地址簿"
], ],
"summary": "创建地址簿", "summary": "批量创建地址簿",
"parameters": [ "parameters": [
{ {
"description": "地址簿信息", "description": "地址簿信息",
@@ -244,6 +244,51 @@ const docTemplateadmin = `{
} }
} }
}, },
"/admin/address_book/share": {
"post": {
"security": [
{
"token": []
}
],
"description": "地址簿分享",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"地址簿"
],
"summary": "地址簿分享",
"parameters": [
{
"description": "地址簿信息",
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/admin.ShareByWebClientForm"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/response.Response"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.Response"
}
}
}
}
},
"/admin/address_book/update": { "/admin/address_book/update": {
"post": { "post": {
"security": [ "security": [
@@ -301,6 +346,157 @@ const docTemplateadmin = `{
} }
} }
}, },
"/admin/app-config": {
"get": {
"security": [
{
"token": []
}
],
"description": "APP服务配置",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"ADMIN"
],
"summary": "APP服务配置",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/response.Response"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.Response"
}
}
}
}
},
"/admin/audit_conn/delete": {
"post": {
"security": [
{
"token": []
}
],
"description": "链接日志删除",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"链接日志"
],
"summary": "链接日志删除",
"parameters": [
{
"description": "链接日志信息",
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/model.AuditConn"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/response.Response"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.Response"
}
}
}
}
},
"/admin/audit_conn/list": {
"get": {
"security": [
{
"token": []
}
],
"description": "链接日志列表",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"链接日志"
],
"summary": "链接日志列表",
"parameters": [
{
"type": "integer",
"description": "页码",
"name": "page",
"in": "query"
},
{
"type": "integer",
"description": "页大小",
"name": "page_size",
"in": "query"
},
{
"type": "integer",
"description": "目标设备",
"name": "peer_id",
"in": "query"
},
{
"type": "integer",
"description": "来源设备",
"name": "from_peer",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/response.Response"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/model.AuditConnList"
}
}
}
]
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.Response"
}
}
}
}
},
"/admin/file/oss_token": { "/admin/file/oss_token": {
"get": { "get": {
"security": [ "security": [
@@ -1242,7 +1438,7 @@ const docTemplateadmin = `{
"token": [] "token": []
} }
], ],
"description": "设备删除", "description": "批量设备删除",
"consumes": [ "consumes": [
"application/json" "application/json"
], ],
@@ -1252,15 +1448,15 @@ const docTemplateadmin = `{
"tags": [ "tags": [
"设备" "设备"
], ],
"summary": "设备删除", "summary": "批量设备删除",
"parameters": [ "parameters": [
{ {
"description": "设备信息", "description": "设备id",
"name": "body", "name": "body",
"in": "body", "in": "body",
"required": true, "required": true,
"schema": { "schema": {
"$ref": "#/definitions/admin.PeerForm" "$ref": "#/definitions/admin.PeerBatchDeleteForm"
} }
} }
], ],
@@ -1365,6 +1561,24 @@ const docTemplateadmin = `{
"description": "页大小", "description": "页大小",
"name": "page_size", "name": "page_size",
"in": "query" "in": "query"
},
{
"type": "integer",
"description": "时间",
"name": "time_ago",
"in": "query"
},
{
"type": "string",
"description": "ID",
"name": "id",
"in": "query"
},
{
"type": "string",
"description": "主机名",
"name": "hostname",
"in": "query"
} }
], ],
"responses": { "responses": {
@@ -1469,7 +1683,7 @@ const docTemplateadmin = `{
"tags": [ "tags": [
"ADMIN" "ADMIN"
], ],
"summary": "服务配置", "summary": "RUSTDESK服务配置",
"responses": { "responses": {
"200": { "200": {
"description": "OK", "description": "OK",
@@ -2311,6 +2525,12 @@ const docTemplateadmin = `{
"user_id": { "user_id": {
"type": "integer" "type": "integer"
}, },
"user_ids": {
"type": "array",
"items": {
"type": "integer"
}
},
"username": { "username": {
"type": "string" "type": "string"
} }
@@ -2398,6 +2618,20 @@ const docTemplateadmin = `{
} }
} }
}, },
"admin.PeerBatchDeleteForm": {
"type": "object",
"required": [
"row_ids"
],
"properties": {
"row_ids": {
"type": "array",
"items": {
"type": "integer"
}
}
}
},
"admin.PeerForm": { "admin.PeerForm": {
"type": "object", "type": "object",
"properties": { "properties": {
@@ -2430,6 +2664,33 @@ const docTemplateadmin = `{
} }
} }
}, },
"admin.ShareByWebClientForm": {
"type": "object",
"required": [
"id",
"password",
"password_type"
],
"properties": {
"expire": {
"type": "integer"
},
"id": {
"type": "string"
},
"password": {
"type": "string"
},
"password_type": {
"description": "只能是once,fixed",
"type": "string",
"enum": [
"once",
"fixed"
]
}
}
},
"admin.TagForm": { "admin.TagForm": {
"type": "object", "type": "object",
"required": [ "required": [
@@ -2600,6 +2861,70 @@ const docTemplateadmin = `{
} }
} }
}, },
"model.AuditConn": {
"type": "object",
"properties": {
"action": {
"type": "string"
},
"close_time": {
"type": "integer"
},
"conn_id": {
"type": "integer"
},
"created_at": {
"type": "string"
},
"from_name": {
"type": "string"
},
"from_peer": {
"type": "string"
},
"id": {
"type": "integer"
},
"ip": {
"type": "string"
},
"peer_id": {
"type": "string"
},
"session_id": {
"type": "string"
},
"type": {
"type": "integer"
},
"updated_at": {
"type": "string"
},
"uuid": {
"type": "string"
}
}
},
"model.AuditConnList": {
"type": "object",
"properties": {
"list": {
"type": "array",
"items": {
"$ref": "#/definitions/model.AuditConn"
}
},
"page": {
"type": "integer"
},
"page_size": {
"type": "integer"
},
"total": {
"type": "integer"
}
}
},
"model.Group": { "model.Group": {
"type": "object", "type": "object",
"properties": { "properties": {
@@ -2759,6 +3084,9 @@ const docTemplateadmin = `{
"id": { "id": {
"type": "string" "type": "string"
}, },
"last_online_time": {
"type": "integer"
},
"memory": { "memory": {
"type": "string" "type": "string"
}, },

View File

@@ -15,7 +15,7 @@
"token": [] "token": []
} }
], ],
"description": "创建地址簿", "description": "批量创建地址簿",
"consumes": [ "consumes": [
"application/json" "application/json"
], ],
@@ -25,7 +25,7 @@
"tags": [ "tags": [
"地址簿" "地址簿"
], ],
"summary": "创建地址簿", "summary": "批量创建地址簿",
"parameters": [ "parameters": [
{ {
"description": "地址簿信息", "description": "地址簿信息",
@@ -237,6 +237,51 @@
} }
} }
}, },
"/admin/address_book/share": {
"post": {
"security": [
{
"token": []
}
],
"description": "地址簿分享",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"地址簿"
],
"summary": "地址簿分享",
"parameters": [
{
"description": "地址簿信息",
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/admin.ShareByWebClientForm"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/response.Response"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.Response"
}
}
}
}
},
"/admin/address_book/update": { "/admin/address_book/update": {
"post": { "post": {
"security": [ "security": [
@@ -294,6 +339,157 @@
} }
} }
}, },
"/admin/app-config": {
"get": {
"security": [
{
"token": []
}
],
"description": "APP服务配置",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"ADMIN"
],
"summary": "APP服务配置",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/response.Response"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.Response"
}
}
}
}
},
"/admin/audit_conn/delete": {
"post": {
"security": [
{
"token": []
}
],
"description": "链接日志删除",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"链接日志"
],
"summary": "链接日志删除",
"parameters": [
{
"description": "链接日志信息",
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/model.AuditConn"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/response.Response"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.Response"
}
}
}
}
},
"/admin/audit_conn/list": {
"get": {
"security": [
{
"token": []
}
],
"description": "链接日志列表",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"链接日志"
],
"summary": "链接日志列表",
"parameters": [
{
"type": "integer",
"description": "页码",
"name": "page",
"in": "query"
},
{
"type": "integer",
"description": "页大小",
"name": "page_size",
"in": "query"
},
{
"type": "integer",
"description": "目标设备",
"name": "peer_id",
"in": "query"
},
{
"type": "integer",
"description": "来源设备",
"name": "from_peer",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/response.Response"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/model.AuditConnList"
}
}
}
]
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.Response"
}
}
}
}
},
"/admin/file/oss_token": { "/admin/file/oss_token": {
"get": { "get": {
"security": [ "security": [
@@ -1235,7 +1431,7 @@
"token": [] "token": []
} }
], ],
"description": "设备删除", "description": "批量设备删除",
"consumes": [ "consumes": [
"application/json" "application/json"
], ],
@@ -1245,15 +1441,15 @@
"tags": [ "tags": [
"设备" "设备"
], ],
"summary": "设备删除", "summary": "批量设备删除",
"parameters": [ "parameters": [
{ {
"description": "设备信息", "description": "设备id",
"name": "body", "name": "body",
"in": "body", "in": "body",
"required": true, "required": true,
"schema": { "schema": {
"$ref": "#/definitions/admin.PeerForm" "$ref": "#/definitions/admin.PeerBatchDeleteForm"
} }
} }
], ],
@@ -1358,6 +1554,24 @@
"description": "页大小", "description": "页大小",
"name": "page_size", "name": "page_size",
"in": "query" "in": "query"
},
{
"type": "integer",
"description": "时间",
"name": "time_ago",
"in": "query"
},
{
"type": "string",
"description": "ID",
"name": "id",
"in": "query"
},
{
"type": "string",
"description": "主机名",
"name": "hostname",
"in": "query"
} }
], ],
"responses": { "responses": {
@@ -1462,7 +1676,7 @@
"tags": [ "tags": [
"ADMIN" "ADMIN"
], ],
"summary": "服务配置", "summary": "RUSTDESK服务配置",
"responses": { "responses": {
"200": { "200": {
"description": "OK", "description": "OK",
@@ -2304,6 +2518,12 @@
"user_id": { "user_id": {
"type": "integer" "type": "integer"
}, },
"user_ids": {
"type": "array",
"items": {
"type": "integer"
}
},
"username": { "username": {
"type": "string" "type": "string"
} }
@@ -2391,6 +2611,20 @@
} }
} }
}, },
"admin.PeerBatchDeleteForm": {
"type": "object",
"required": [
"row_ids"
],
"properties": {
"row_ids": {
"type": "array",
"items": {
"type": "integer"
}
}
}
},
"admin.PeerForm": { "admin.PeerForm": {
"type": "object", "type": "object",
"properties": { "properties": {
@@ -2423,6 +2657,33 @@
} }
} }
}, },
"admin.ShareByWebClientForm": {
"type": "object",
"required": [
"id",
"password",
"password_type"
],
"properties": {
"expire": {
"type": "integer"
},
"id": {
"type": "string"
},
"password": {
"type": "string"
},
"password_type": {
"description": "只能是once,fixed",
"type": "string",
"enum": [
"once",
"fixed"
]
}
}
},
"admin.TagForm": { "admin.TagForm": {
"type": "object", "type": "object",
"required": [ "required": [
@@ -2593,6 +2854,70 @@
} }
} }
}, },
"model.AuditConn": {
"type": "object",
"properties": {
"action": {
"type": "string"
},
"close_time": {
"type": "integer"
},
"conn_id": {
"type": "integer"
},
"created_at": {
"type": "string"
},
"from_name": {
"type": "string"
},
"from_peer": {
"type": "string"
},
"id": {
"type": "integer"
},
"ip": {
"type": "string"
},
"peer_id": {
"type": "string"
},
"session_id": {
"type": "string"
},
"type": {
"type": "integer"
},
"updated_at": {
"type": "string"
},
"uuid": {
"type": "string"
}
}
},
"model.AuditConnList": {
"type": "object",
"properties": {
"list": {
"type": "array",
"items": {
"$ref": "#/definitions/model.AuditConn"
}
},
"page": {
"type": "integer"
},
"page_size": {
"type": "integer"
},
"total": {
"type": "integer"
}
}
},
"model.Group": { "model.Group": {
"type": "object", "type": "object",
"properties": { "properties": {
@@ -2752,6 +3077,9 @@
"id": { "id": {
"type": "string" "type": "string"
}, },
"last_online_time": {
"type": "integer"
},
"memory": { "memory": {
"type": "string" "type": "string"
}, },

View File

@@ -46,6 +46,10 @@ definitions:
type: array type: array
user_id: user_id:
type: integer type: integer
user_ids:
items:
type: integer
type: array
username: username:
type: string type: string
required: required:
@@ -107,6 +111,15 @@ definitions:
- op - op
- redirect_url - redirect_url
type: object type: object
admin.PeerBatchDeleteForm:
properties:
row_ids:
items:
type: integer
type: array
required:
- row_ids
type: object
admin.PeerForm: admin.PeerForm:
properties: properties:
cpu: cpu:
@@ -128,6 +141,25 @@ definitions:
version: version:
type: string type: string
type: object type: object
admin.ShareByWebClientForm:
properties:
expire:
type: integer
id:
type: string
password:
type: string
password_type:
description: 只能是once,fixed
enum:
- once
- fixed
type: string
required:
- id
- password
- password_type
type: object
admin.TagForm: admin.TagForm:
properties: properties:
color: color:
@@ -241,6 +273,48 @@ definitions:
total: total:
type: integer type: integer
type: object type: object
model.AuditConn:
properties:
action:
type: string
close_time:
type: integer
conn_id:
type: integer
created_at:
type: string
from_name:
type: string
from_peer:
type: string
id:
type: integer
ip:
type: string
peer_id:
type: string
session_id:
type: string
type:
type: integer
updated_at:
type: string
uuid:
type: string
type: object
model.AuditConnList:
properties:
list:
items:
$ref: '#/definitions/model.AuditConn'
type: array
page:
type: integer
page_size:
type: integer
total:
type: integer
type: object
model.Group: model.Group:
properties: properties:
created_at: created_at:
@@ -346,6 +420,8 @@ definitions:
type: string type: string
id: id:
type: string type: string
last_online_time:
type: integer
memory: memory:
type: string type: string
os: os:
@@ -471,7 +547,7 @@ paths:
post: post:
consumes: consumes:
- application/json - application/json
description: 创建地址簿 description: 批量创建地址簿
parameters: parameters:
- description: 地址簿信息 - description: 地址簿信息
in: body in: body
@@ -497,7 +573,7 @@ paths:
$ref: '#/definitions/response.Response' $ref: '#/definitions/response.Response'
security: security:
- token: [] - token: []
summary: 创建地址簿 summary: 批量创建地址簿
tags: tags:
- 地址簿 - 地址簿
/admin/address_book/delete: /admin/address_book/delete:
@@ -603,6 +679,34 @@ paths:
summary: 地址簿列表 summary: 地址簿列表
tags: tags:
- 地址簿 - 地址簿
/admin/address_book/share:
post:
consumes:
- application/json
description: 地址簿分享
parameters:
- description: 地址簿信息
in: body
name: body
required: true
schema:
$ref: '#/definitions/admin.ShareByWebClientForm'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/response.Response'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.Response'
security:
- token: []
summary: 地址簿分享
tags:
- 地址簿
/admin/address_book/update: /admin/address_book/update:
post: post:
consumes: consumes:
@@ -636,6 +740,98 @@ paths:
summary: 地址簿编辑 summary: 地址簿编辑
tags: tags:
- 地址簿 - 地址簿
/admin/app-config:
get:
consumes:
- application/json
description: APP服务配置
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/response.Response'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.Response'
security:
- token: []
summary: APP服务配置
tags:
- ADMIN
/admin/audit_conn/delete:
post:
consumes:
- application/json
description: 链接日志删除
parameters:
- description: 链接日志信息
in: body
name: body
required: true
schema:
$ref: '#/definitions/model.AuditConn'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/response.Response'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.Response'
security:
- token: []
summary: 链接日志删除
tags:
- 链接日志
/admin/audit_conn/list:
get:
consumes:
- application/json
description: 链接日志列表
parameters:
- description: 页码
in: query
name: page
type: integer
- description: 页大小
in: query
name: page_size
type: integer
- description: 目标设备
in: query
name: peer_id
type: integer
- description: 来源设备
in: query
name: from_peer
type: integer
produces:
- application/json
responses:
"200":
description: OK
schema:
allOf:
- $ref: '#/definitions/response.Response'
- properties:
data:
$ref: '#/definitions/model.AuditConnList'
type: object
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.Response'
security:
- token: []
summary: 链接日志列表
tags:
- 链接日志
/admin/file/oss_token: /admin/file/oss_token:
get: get:
consumes: consumes:
@@ -1194,14 +1390,14 @@ paths:
post: post:
consumes: consumes:
- application/json - application/json
description: 设备删除 description: 批量设备删除
parameters: parameters:
- description: 设备信息 - description: 设备id
in: body in: body
name: body name: body
required: true required: true
schema: schema:
$ref: '#/definitions/admin.PeerForm' $ref: '#/definitions/admin.PeerBatchDeleteForm'
produces: produces:
- application/json - application/json
responses: responses:
@@ -1215,7 +1411,7 @@ paths:
$ref: '#/definitions/response.Response' $ref: '#/definitions/response.Response'
security: security:
- token: [] - token: []
summary: 设备删除 summary: 批量设备删除
tags: tags:
- 设备 - 设备
/admin/peer/detail/{id}: /admin/peer/detail/{id}:
@@ -1264,6 +1460,18 @@ paths:
in: query in: query
name: page_size name: page_size
type: integer type: integer
- description: 时间
in: query
name: time_ago
type: integer
- description: ID
in: query
name: id
type: string
- description: 主机名
in: query
name: hostname
type: string
produces: produces:
- application/json - application/json
responses: responses:
@@ -1336,7 +1544,7 @@ paths:
$ref: '#/definitions/response.Response' $ref: '#/definitions/response.Response'
security: security:
- token: [] - token: []
summary: 服务配置 summary: RUSTDESK服务配置
tags: tags:
- ADMIN - ADMIN
/admin/tag/create: /admin/tag/create:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 46 KiB

View File

@@ -121,40 +121,6 @@ const docTemplateapi = `{
} }
} }
}, },
"/ab/add": {
"post": {
"security": [
{
"BearerAuth": []
}
],
"description": "标签",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"地址[Personal]"
],
"summary": "标签添加",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "string"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.ErrorResponse"
}
}
}
}
},
"/ab/peer/add/{guid}": { "/ab/peer/add/{guid}": {
"post": { "post": {
"security": [ "security": [
@@ -176,8 +142,8 @@ const docTemplateapi = `{
"parameters": [ "parameters": [
{ {
"type": "string", "type": "string",
"description": "id", "description": "guid",
"name": "id", "name": "guid",
"in": "path", "in": "path",
"required": true "required": true
} }
@@ -217,8 +183,8 @@ const docTemplateapi = `{
"parameters": [ "parameters": [
{ {
"type": "string", "type": "string",
"description": "id", "description": "guid",
"name": "id", "name": "guid",
"in": "path", "in": "path",
"required": true "required": true
} }
@@ -260,8 +226,8 @@ const docTemplateapi = `{
"parameters": [ "parameters": [
{ {
"type": "string", "type": "string",
"description": "id", "description": "guid",
"name": "id", "name": "guid",
"in": "path", "in": "path",
"required": true "required": true
} }
@@ -302,12 +268,22 @@ const docTemplateapi = `{
"summary": "地址列表", "summary": "地址列表",
"parameters": [ "parameters": [
{ {
"description": "string valid", "type": "integer",
"name": "string", "description": "页码",
"in": "body", "name": "current",
"schema": { "in": "query"
"type": "string" },
} {
"type": "integer",
"description": "每页数量",
"name": "pageSize",
"in": "query"
},
{
"type": "string",
"description": "guid",
"name": "ab",
"in": "query"
} }
], ],
"responses": { "responses": {
@@ -434,12 +410,16 @@ const docTemplateapi = `{
"summary": "共享地址簿", "summary": "共享地址簿",
"parameters": [ "parameters": [
{ {
"description": "string valid", "type": "integer",
"name": "string", "description": "页码",
"in": "body", "name": "current",
"schema": { "in": "query"
"type": "string" },
} {
"type": "integer",
"description": "每页数量",
"name": "pageSize",
"in": "query"
} }
], ],
"responses": { "responses": {
@@ -458,6 +438,49 @@ const docTemplateapi = `{
} }
} }
}, },
"/ab/tag/add/{guid}": {
"post": {
"security": [
{
"BearerAuth": []
}
],
"description": "标签",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"地址[Personal]"
],
"summary": "标签添加",
"parameters": [
{
"type": "string",
"description": "guid",
"name": "guid",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "string"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.ErrorResponse"
}
}
}
}
},
"/ab/tag/rename/{guid}": { "/ab/tag/rename/{guid}": {
"put": { "put": {
"security": [ "security": [
@@ -476,6 +499,15 @@ const docTemplateapi = `{
"地址[Personal]" "地址[Personal]"
], ],
"summary": "标签重命名", "summary": "标签重命名",
"parameters": [
{
"type": "string",
"description": "guid",
"name": "guid",
"in": "path",
"required": true
}
],
"responses": { "responses": {
"200": { "200": {
"description": "OK", "description": "OK",
@@ -510,6 +542,15 @@ const docTemplateapi = `{
"地址[Personal]" "地址[Personal]"
], ],
"summary": "标签修改颜色", "summary": "标签修改颜色",
"parameters": [
{
"type": "string",
"description": "guid",
"name": "guid",
"in": "path",
"required": true
}
],
"responses": { "responses": {
"200": { "200": {
"description": "OK", "description": "OK",
@@ -544,6 +585,15 @@ const docTemplateapi = `{
"地址[Personal]" "地址[Personal]"
], ],
"summary": "标签删除", "summary": "标签删除",
"parameters": [
{
"type": "string",
"description": "guid",
"name": "guid",
"in": "path",
"required": true
}
],
"responses": { "responses": {
"200": { "200": {
"description": "OK", "description": "OK",
@@ -581,8 +631,8 @@ const docTemplateapi = `{
"parameters": [ "parameters": [
{ {
"type": "string", "type": "string",
"description": "id", "description": "guid",
"name": "id", "name": "guid",
"in": "path", "in": "path",
"required": true "required": true
} }
@@ -637,6 +687,46 @@ const docTemplateapi = `{
} }
} }
}, },
"/audit/conn": {
"post": {
"description": "审计连接",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"审计"
],
"summary": "审计连接",
"parameters": [
{
"description": "审计连接",
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/api.AuditConnForm"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "string"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.Response"
}
}
}
}
},
"/heartbeat": { "/heartbeat": {
"post": { "post": {
"description": "心跳", "description": "心跳",
@@ -945,13 +1035,37 @@ const docTemplateapi = `{
} }
} }
}, },
"/shared-peer": {
"post": {
"description": "分享的peer",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"WEBCLIENT"
],
"summary": "分享的peer",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/response.Response"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.Response"
}
}
}
}
},
"/sysinfo": { "/sysinfo": {
"post": { "post": {
"security": [
{
"BearerAuth": []
}
],
"description": "提交系统信息", "description": "提交系统信息",
"consumes": [ "consumes": [
"application/json" "application/json"
@@ -1113,6 +1227,38 @@ const docTemplateapi = `{
} }
} }
}, },
"api.AuditConnForm": {
"type": "object",
"properties": {
"action": {
"type": "string"
},
"conn_id": {
"type": "integer"
},
"id": {
"type": "string"
},
"ip": {
"type": "string"
},
"peer": {
"type": "array",
"items": {
"type": "string"
}
},
"session_id": {
"type": "number"
},
"type": {
"type": "integer"
},
"uuid": {
"type": "string"
}
}
},
"api.DeviceInfoInLogin": { "api.DeviceInfoInLogin": {
"type": "object", "type": "object",
"properties": { "properties": {

View File

@@ -114,40 +114,6 @@
} }
} }
}, },
"/ab/add": {
"post": {
"security": [
{
"BearerAuth": []
}
],
"description": "标签",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"地址[Personal]"
],
"summary": "标签添加",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "string"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.ErrorResponse"
}
}
}
}
},
"/ab/peer/add/{guid}": { "/ab/peer/add/{guid}": {
"post": { "post": {
"security": [ "security": [
@@ -169,8 +135,8 @@
"parameters": [ "parameters": [
{ {
"type": "string", "type": "string",
"description": "id", "description": "guid",
"name": "id", "name": "guid",
"in": "path", "in": "path",
"required": true "required": true
} }
@@ -210,8 +176,8 @@
"parameters": [ "parameters": [
{ {
"type": "string", "type": "string",
"description": "id", "description": "guid",
"name": "id", "name": "guid",
"in": "path", "in": "path",
"required": true "required": true
} }
@@ -253,8 +219,8 @@
"parameters": [ "parameters": [
{ {
"type": "string", "type": "string",
"description": "id", "description": "guid",
"name": "id", "name": "guid",
"in": "path", "in": "path",
"required": true "required": true
} }
@@ -295,12 +261,22 @@
"summary": "地址列表", "summary": "地址列表",
"parameters": [ "parameters": [
{ {
"description": "string valid", "type": "integer",
"name": "string", "description": "页码",
"in": "body", "name": "current",
"schema": { "in": "query"
"type": "string" },
} {
"type": "integer",
"description": "每页数量",
"name": "pageSize",
"in": "query"
},
{
"type": "string",
"description": "guid",
"name": "ab",
"in": "query"
} }
], ],
"responses": { "responses": {
@@ -427,12 +403,16 @@
"summary": "共享地址簿", "summary": "共享地址簿",
"parameters": [ "parameters": [
{ {
"description": "string valid", "type": "integer",
"name": "string", "description": "页码",
"in": "body", "name": "current",
"schema": { "in": "query"
"type": "string" },
} {
"type": "integer",
"description": "每页数量",
"name": "pageSize",
"in": "query"
} }
], ],
"responses": { "responses": {
@@ -451,6 +431,49 @@
} }
} }
}, },
"/ab/tag/add/{guid}": {
"post": {
"security": [
{
"BearerAuth": []
}
],
"description": "标签",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"地址[Personal]"
],
"summary": "标签添加",
"parameters": [
{
"type": "string",
"description": "guid",
"name": "guid",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "string"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.ErrorResponse"
}
}
}
}
},
"/ab/tag/rename/{guid}": { "/ab/tag/rename/{guid}": {
"put": { "put": {
"security": [ "security": [
@@ -469,6 +492,15 @@
"地址[Personal]" "地址[Personal]"
], ],
"summary": "标签重命名", "summary": "标签重命名",
"parameters": [
{
"type": "string",
"description": "guid",
"name": "guid",
"in": "path",
"required": true
}
],
"responses": { "responses": {
"200": { "200": {
"description": "OK", "description": "OK",
@@ -503,6 +535,15 @@
"地址[Personal]" "地址[Personal]"
], ],
"summary": "标签修改颜色", "summary": "标签修改颜色",
"parameters": [
{
"type": "string",
"description": "guid",
"name": "guid",
"in": "path",
"required": true
}
],
"responses": { "responses": {
"200": { "200": {
"description": "OK", "description": "OK",
@@ -537,6 +578,15 @@
"地址[Personal]" "地址[Personal]"
], ],
"summary": "标签删除", "summary": "标签删除",
"parameters": [
{
"type": "string",
"description": "guid",
"name": "guid",
"in": "path",
"required": true
}
],
"responses": { "responses": {
"200": { "200": {
"description": "OK", "description": "OK",
@@ -574,8 +624,8 @@
"parameters": [ "parameters": [
{ {
"type": "string", "type": "string",
"description": "id", "description": "guid",
"name": "id", "name": "guid",
"in": "path", "in": "path",
"required": true "required": true
} }
@@ -630,6 +680,46 @@
} }
} }
}, },
"/audit/conn": {
"post": {
"description": "审计连接",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"审计"
],
"summary": "审计连接",
"parameters": [
{
"description": "审计连接",
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/api.AuditConnForm"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "string"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.Response"
}
}
}
}
},
"/heartbeat": { "/heartbeat": {
"post": { "post": {
"description": "心跳", "description": "心跳",
@@ -938,13 +1028,37 @@
} }
} }
}, },
"/shared-peer": {
"post": {
"description": "分享的peer",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"WEBCLIENT"
],
"summary": "分享的peer",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/response.Response"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.Response"
}
}
}
}
},
"/sysinfo": { "/sysinfo": {
"post": { "post": {
"security": [
{
"BearerAuth": []
}
],
"description": "提交系统信息", "description": "提交系统信息",
"consumes": [ "consumes": [
"application/json" "application/json"
@@ -1106,6 +1220,38 @@
} }
} }
}, },
"api.AuditConnForm": {
"type": "object",
"properties": {
"action": {
"type": "string"
},
"conn_id": {
"type": "integer"
},
"id": {
"type": "string"
},
"ip": {
"type": "string"
},
"peer": {
"type": "array",
"items": {
"type": "string"
}
},
"session_id": {
"type": "number"
},
"type": {
"type": "integer"
},
"uuid": {
"type": "string"
}
}
},
"api.DeviceInfoInLogin": { "api.DeviceInfoInLogin": {
"type": "object", "type": "object",
"properties": { "properties": {

View File

@@ -6,6 +6,27 @@ definitions:
example: '{"tags":["tag1","tag2","tag3"],"peers":[{"id":"abc","username":"abv-l","hostname":"","platform":"Windows","alias":"","tags":["tag1","tag2"],"hash":"hash"}],"tag_colors":"{\"tag1\":4288585374,\"tag2\":4278238420,\"tag3\":4291681337}"}' example: '{"tags":["tag1","tag2","tag3"],"peers":[{"id":"abc","username":"abv-l","hostname":"","platform":"Windows","alias":"","tags":["tag1","tag2"],"hash":"hash"}],"tag_colors":"{\"tag1\":4288585374,\"tag2\":4278238420,\"tag3\":4291681337}"}'
type: string type: string
type: object type: object
api.AuditConnForm:
properties:
action:
type: string
conn_id:
type: integer
id:
type: string
ip:
type: string
peer:
items:
type: string
type: array
session_id:
type: number
type:
type: integer
uuid:
type: string
type: object
api.DeviceInfoInLogin: api.DeviceInfoInLogin:
properties: properties:
name: name:
@@ -208,36 +229,15 @@ paths:
summary: 地址更新 summary: 地址更新
tags: tags:
- 地址 - 地址
/ab/add:
post:
consumes:
- application/json
description: 标签
produces:
- application/json
responses:
"200":
description: OK
schema:
type: string
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.ErrorResponse'
security:
- BearerAuth: []
summary: 标签添加
tags:
- 地址[Personal]
/ab/peer/add/{guid}: /ab/peer/add/{guid}:
delete: delete:
consumes: consumes:
- application/json - application/json
description: 删除地址 description: 删除地址
parameters: parameters:
- description: id - description: guid
in: path in: path
name: id name: guid
required: true required: true
type: string type: string
produces: produces:
@@ -261,9 +261,9 @@ paths:
- application/json - application/json
description: 添加地址 description: 添加地址
parameters: parameters:
- description: id - description: guid
in: path in: path
name: id name: guid
required: true required: true
type: string type: string
produces: produces:
@@ -288,9 +288,9 @@ paths:
- application/json - application/json
description: 更新地址 description: 更新地址
parameters: parameters:
- description: id - description: guid
in: path in: path
name: id name: guid
required: true required: true
type: string type: string
produces: produces:
@@ -315,11 +315,18 @@ paths:
- application/json - application/json
description: 地址 description: 地址
parameters: parameters:
- description: string valid - description: 页码
in: body in: query
name: string name: current
schema: type: integer
type: string - description: 每页数量
in: query
name: pageSize
type: integer
- description: guid
in: query
name: ab
type: string
produces: produces:
- application/json - application/json
responses: responses:
@@ -396,11 +403,14 @@ paths:
- application/json - application/json
description: 共享 description: 共享
parameters: parameters:
- description: string valid - description: 页码
in: body in: query
name: string name: current
schema: type: integer
type: string - description: 每页数量
in: query
name: pageSize
type: integer
produces: produces:
- application/json - application/json
responses: responses:
@@ -422,6 +432,12 @@ paths:
consumes: consumes:
- application/json - application/json
description: 标签 description: 标签
parameters:
- description: guid
in: path
name: guid
required: true
type: string
produces: produces:
- application/json - application/json
responses: responses:
@@ -438,11 +454,44 @@ paths:
summary: 标签删除 summary: 标签删除
tags: tags:
- 地址[Personal] - 地址[Personal]
/ab/tag/add/{guid}:
post:
consumes:
- application/json
description: 标签
parameters:
- description: guid
in: path
name: guid
required: true
type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
type: string
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.ErrorResponse'
security:
- BearerAuth: []
summary: 标签添加
tags:
- 地址[Personal]
/ab/tag/rename/{guid}: /ab/tag/rename/{guid}:
put: put:
consumes: consumes:
- application/json - application/json
description: 标签 description: 标签
parameters:
- description: guid
in: path
name: guid
required: true
type: string
produces: produces:
- application/json - application/json
responses: responses:
@@ -464,6 +513,12 @@ paths:
consumes: consumes:
- application/json - application/json
description: 标签 description: 标签
parameters:
- description: guid
in: path
name: guid
required: true
type: string
produces: produces:
- application/json - application/json
responses: responses:
@@ -486,9 +541,9 @@ paths:
- application/json - application/json
description: 标签 description: 标签
parameters: parameters:
- description: id - description: guid
in: path in: path
name: id name: guid
required: true required: true
type: string type: string
produces: produces:
@@ -528,6 +583,32 @@ paths:
summary: 用户信息 summary: 用户信息
tags: tags:
- 用户 - 用户
/audit/conn:
post:
consumes:
- application/json
description: 审计连接
parameters:
- description: 审计连接
in: body
name: body
required: true
schema:
$ref: '#/definitions/api.AuditConnForm'
produces:
- application/json
responses:
"200":
description: OK
schema:
type: string
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.Response'
summary: 审计连接
tags:
- 审计
/heartbeat: /heartbeat:
post: post:
consumes: consumes:
@@ -727,6 +808,25 @@ paths:
summary: 服务配置 summary: 服务配置
tags: tags:
- WEBCLIENT - WEBCLIENT
/shared-peer:
post:
consumes:
- application/json
description: 分享的peer
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/response.Response'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.Response'
summary: 分享的peer
tags:
- WEBCLIENT
/sysinfo: /sysinfo:
post: post:
consumes: consumes:
@@ -750,8 +850,6 @@ paths:
description: Internal Server Error description: Internal Server Error
schema: schema:
$ref: '#/definitions/response.ErrorResponse' $ref: '#/definitions/response.ErrorResponse'
security:
- BearerAuth: []
summary: 提交系统信息 summary: 提交系统信息
tags: tags:
- 地址 - 地址

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

BIN
docs/en_img/pc_ab.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

BIN
docs/en_img/pc_gr.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

BIN
docs/en_img/pc_login.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

BIN
docs/en_img/web_admin.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

1
go.mod
View File

@@ -13,6 +13,7 @@ require (
github.com/go-playground/validator/v10 v10.11.2 github.com/go-playground/validator/v10 v10.11.2
github.com/go-redis/redis/v8 v8.11.4 github.com/go-redis/redis/v8 v8.11.4
github.com/golang-jwt/jwt/v5 v5.2.1 github.com/golang-jwt/jwt/v5 v5.2.1
github.com/google/uuid v1.1.2
github.com/nicksnyder/go-i18n/v2 v2.4.0 github.com/nicksnyder/go-i18n/v2 v2.4.0
github.com/sirupsen/logrus v1.8.1 github.com/sirupsen/logrus v1.8.1
github.com/spf13/viper v1.9.0 github.com/spf13/viper v1.9.0

View File

@@ -4,6 +4,7 @@ import (
"Gwen/global" "Gwen/global"
"Gwen/http/request/admin" "Gwen/http/request/admin"
"Gwen/http/response" "Gwen/http/response"
"Gwen/model"
"Gwen/service" "Gwen/service"
_ "encoding/json" _ "encoding/json"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
@@ -69,6 +70,12 @@ func (ct *AddressBook) Create(c *gin.Context) {
if !service.AllService.UserService.IsAdmin(u) || t.UserId == 0 { if !service.AllService.UserService.IsAdmin(u) || t.UserId == 0 {
t.UserId = u.Id t.UserId = u.Id
} }
ex := service.AllService.AddressBookService.InfoByUserIdAndId(t.UserId, t.Id)
if ex.RowId > 0 {
response.Fail(c, 101, response.TranslateMsg(c, "ItemExist"))
return
}
err := service.AllService.AddressBookService.Create(t) err := service.AllService.AddressBookService.Create(t)
if err != nil { if err != nil {
response.Fail(c, 101, response.TranslateMsg(c, "OperationFailed")+err.Error()) response.Fail(c, 101, response.TranslateMsg(c, "OperationFailed")+err.Error())
@@ -77,6 +84,58 @@ func (ct *AddressBook) Create(c *gin.Context) {
response.Success(c, u) response.Success(c, u)
} }
// BatchCreate 批量创建地址簿
// @Tags 地址簿
// @Summary 批量创建地址簿
// @Description 批量创建地址簿
// @Accept json
// @Produce json
// @Param body body admin.AddressBookForm true "地址簿信息"
// @Success 200 {object} response.Response{data=model.AddressBook}
// @Failure 500 {object} response.Response
// @Router /admin/address_book/create [post]
// @Security token
func (ct *AddressBook) BatchCreate(c *gin.Context) {
f := &admin.AddressBookForm{}
if err := c.ShouldBindJSON(f); err != nil {
response.Fail(c, 101, response.TranslateMsg(c, "ParamsError")+err.Error())
return
}
errList := global.Validator.ValidStruct(c, f)
if len(errList) > 0 {
response.Fail(c, 101, errList[0])
return
}
//创建标签
for _, fu := range f.UserIds {
if fu == 0 {
continue
}
for _, ft := range f.Tags {
exTag := service.AllService.TagService.InfoByUserIdAndName(fu, ft)
if exTag.Id == 0 {
service.AllService.TagService.Create(&model.Tag{
UserId: fu,
Name: ft,
})
}
}
}
ts := f.ToAddressBooks()
for _, t := range ts {
if t.UserId == 0 {
continue
}
ex := service.AllService.AddressBookService.InfoByUserIdAndId(t.UserId, t.Id)
if ex.RowId == 0 {
service.AllService.AddressBookService.Create(t)
}
}
response.Success(c, nil)
}
// List 列表 // List 列表
// @Tags 地址簿 // @Tags 地址簿
// @Summary 地址簿列表 // @Summary 地址簿列表
@@ -102,9 +161,18 @@ func (ct *AddressBook) List(c *gin.Context) {
query.UserId = int(u.Id) query.UserId = int(u.Id)
} }
res := service.AllService.AddressBookService.List(query.Page, query.PageSize, func(tx *gorm.DB) { res := service.AllService.AddressBookService.List(query.Page, query.PageSize, func(tx *gorm.DB) {
if query.Id != "" {
tx.Where("id like ?", "%"+query.Id+"%")
}
if query.UserId > 0 { if query.UserId > 0 {
tx.Where("user_id = ?", query.UserId) tx.Where("user_id = ?", query.UserId)
} }
if query.Username != "" {
tx.Where("username like ?", "%"+query.Username+"%")
}
if query.Hostname != "" {
tx.Where("hostname like ?", "%"+query.Hostname+"%")
}
}) })
response.Success(c, res) response.Success(c, res)
} }
@@ -189,3 +257,44 @@ func (ct *AddressBook) Delete(c *gin.Context) {
} }
response.Fail(c, 101, response.TranslateMsg(c, "ItemNotFound")) response.Fail(c, 101, response.TranslateMsg(c, "ItemNotFound"))
} }
// ShareByWebClient
// @Tags 地址簿
// @Summary 地址簿分享
// @Description 地址簿分享
// @Accept json
// @Produce json
// @Param body body admin.ShareByWebClientForm true "地址簿信息"
// @Success 200 {object} response.Response
// @Failure 500 {object} response.Response
// @Router /admin/address_book/share [post]
// @Security token
func (ct *AddressBook) ShareByWebClient(c *gin.Context) {
f := &admin.ShareByWebClientForm{}
if err := c.ShouldBindJSON(f); err != nil {
response.Fail(c, 101, response.TranslateMsg(c, "ParamsError")+err.Error())
return
}
errList := global.Validator.ValidStruct(c, f)
if len(errList) > 0 {
response.Fail(c, 101, errList[0])
return
}
u := service.AllService.UserService.CurUser(c)
ab := service.AllService.AddressBookService.InfoByUserIdAndId(u.Id, f.Id)
if ab.RowId == 0 {
response.Fail(c, 101, response.TranslateMsg(c, "ItemNotFound"))
return
}
m := f.ToShareRecord()
m.UserId = u.Id
err := service.AllService.AddressBookService.ShareByWebClient(m)
if err != nil {
response.Fail(c, 101, response.TranslateMsg(c, "OperationFailed")+err.Error())
return
}
response.Success(c, &gin.H{
"share_token": m.ShareToken,
})
}

View File

@@ -0,0 +1,81 @@
package admin
import (
"Gwen/global"
"Gwen/http/request/admin"
"Gwen/http/response"
"Gwen/model"
"Gwen/service"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
type Audit struct {
}
// ConnList 列表
// @Tags 链接日志
// @Summary 链接日志列表
// @Description 链接日志列表
// @Accept json
// @Produce json
// @Param page query int false "页码"
// @Param page_size query int false "页大小"
// @Param peer_id query int false "目标设备"
// @Param from_peer query int false "来源设备"
// @Success 200 {object} response.Response{data=model.AuditConnList}
// @Failure 500 {object} response.Response
// @Router /admin/audit_conn/list [get]
// @Security token
func (a *Audit) ConnList(c *gin.Context) {
query := &admin.AuditQuery{}
if err := c.ShouldBindQuery(query); err != nil {
response.Fail(c, 101, response.TranslateMsg(c, "ParamsError")+err.Error())
return
}
res := service.AllService.AuditService.AuditConnList(query.Page, query.PageSize, func(tx *gorm.DB) {
if query.PeerId != "" {
tx.Where("peer_id like ?", "%"+query.PeerId+"%")
}
if query.FromPeer != "" {
tx.Where("from_peer like ?", "%"+query.FromPeer+"%")
}
})
response.Success(c, res)
}
// ConnDelete 删除
// @Tags 链接日志
// @Summary 链接日志删除
// @Description 链接日志删除
// @Accept json
// @Produce json
// @Param body body model.AuditConn true "链接日志信息"
// @Success 200 {object} response.Response
// @Failure 500 {object} response.Response
// @Router /admin/audit_conn/delete [post]
// @Security token
func (a *Audit) ConnDelete(c *gin.Context) {
f := &model.AuditConn{}
if err := c.ShouldBindJSON(f); err != nil {
response.Fail(c, 101, response.TranslateMsg(c, "ParamsError")+err.Error())
return
}
id := f.Id
errList := global.Validator.ValidVar(c, id, "required,gt=0")
if len(errList) > 0 {
response.Fail(c, 101, errList[0])
return
}
l := service.AllService.AuditService.InfoById(f.Id)
if l.Id > 0 {
err := service.AllService.AuditService.DeleteAuditConn(l)
if err == nil {
response.Success(c, nil)
return
}
response.Fail(c, 101, err.Error())
return
}
response.Fail(c, 101, response.TranslateMsg(c, "ItemNotFound"))
}

View File

@@ -7,6 +7,7 @@ import (
adResp "Gwen/http/response/admin" adResp "Gwen/http/response/admin"
"Gwen/model" "Gwen/model"
"Gwen/service" "Gwen/service"
"fmt"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
@@ -28,18 +29,21 @@ func (ct *Login) Login(c *gin.Context) {
f := &admin.Login{} f := &admin.Login{}
err := c.ShouldBindJSON(f) err := c.ShouldBindJSON(f)
if err != nil { if err != nil {
global.Logger.Warn(fmt.Sprintf("Login Fail: %s %s %s", "ParamsError", c.RemoteIP(), c.ClientIP()))
response.Fail(c, 101, response.TranslateMsg(c, "ParamsError")+err.Error()) response.Fail(c, 101, response.TranslateMsg(c, "ParamsError")+err.Error())
return return
} }
errList := global.Validator.ValidStruct(c, f) errList := global.Validator.ValidStruct(c, f)
if len(errList) > 0 { if len(errList) > 0 {
global.Logger.Warn(fmt.Sprintf("Login Fail: %s %s %s", "ParamsError", c.RemoteIP(), c.ClientIP()))
response.Fail(c, 101, errList[0]) response.Fail(c, 101, errList[0])
return return
} }
u := service.AllService.UserService.InfoByUsernamePassword(f.Username, f.Password) u := service.AllService.UserService.InfoByUsernamePassword(f.Username, f.Password)
if u.Id == 0 { if u.Id == 0 {
global.Logger.Warn(fmt.Sprintf("Login Fail: %s %s %s", "UsernameOrPasswordError", c.RemoteIP(), c.ClientIP()))
response.Fail(c, 101, response.TranslateMsg(c, "UsernameOrPasswordError")) response.Fail(c, 101, response.TranslateMsg(c, "UsernameOrPasswordError"))
return return
} }

View File

@@ -6,7 +6,9 @@ import (
"Gwen/http/response" "Gwen/http/response"
"Gwen/service" "Gwen/service"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"gorm.io/gorm"
"strconv" "strconv"
"time"
) )
type Peer struct { type Peer struct {
@@ -74,17 +76,35 @@ func (ct *Peer) Create(c *gin.Context) {
// @Produce json // @Produce json
// @Param page query int false "页码" // @Param page query int false "页码"
// @Param page_size query int false "页大小" // @Param page_size query int false "页大小"
// @Param time_ago query int false "时间"
// @Param id query string false "ID"
// @Param hostname query string false "主机名"
// @Success 200 {object} response.Response{data=model.PeerList} // @Success 200 {object} response.Response{data=model.PeerList}
// @Failure 500 {object} response.Response // @Failure 500 {object} response.Response
// @Router /admin/peer/list [get] // @Router /admin/peer/list [get]
// @Security token // @Security token
func (ct *Peer) List(c *gin.Context) { func (ct *Peer) List(c *gin.Context) {
query := &admin.PageQuery{} query := &admin.PeerQuery{}
if err := c.ShouldBindQuery(query); err != nil { if err := c.ShouldBindQuery(query); err != nil {
response.Fail(c, 101, response.TranslateMsg(c, "ParamsError")+err.Error()) response.Fail(c, 101, response.TranslateMsg(c, "ParamsError")+err.Error())
return return
} }
res := service.AllService.PeerService.List(query.Page, query.PageSize, nil) res := service.AllService.PeerService.List(query.Page, query.PageSize, func(tx *gorm.DB) {
if query.TimeAgo > 0 {
lt := time.Now().Unix() - int64(query.TimeAgo)
tx.Where("last_online_time < ?", lt)
}
if query.TimeAgo < 0 {
lt := time.Now().Unix() + int64(query.TimeAgo)
tx.Where("last_online_time > ?", lt)
}
if query.Id != "" {
tx.Where("id like ?", "%"+query.Id+"%")
}
if query.Hostname != "" {
tx.Where("hostname like ?", "%"+query.Hostname+"%")
}
})
response.Success(c, res) response.Success(c, res)
} }
@@ -158,3 +178,32 @@ func (ct *Peer) Delete(c *gin.Context) {
} }
response.Fail(c, 101, response.TranslateMsg(c, "ItemNotFound")) response.Fail(c, 101, response.TranslateMsg(c, "ItemNotFound"))
} }
// BatchDelete 批量删除
// @Tags 设备
// @Summary 批量设备删除
// @Description 批量设备删除
// @Accept json
// @Produce json
// @Param body body admin.PeerBatchDeleteForm true "设备id"
// @Success 200 {object} response.Response
// @Failure 500 {object} response.Response
// @Router /admin/peer/delete [post]
// @Security token
func (ct *Peer) BatchDelete(c *gin.Context) {
f := &admin.PeerBatchDeleteForm{}
if err := c.ShouldBindJSON(f); err != nil {
response.Fail(c, 101, response.TranslateMsg(c, "ParamsError")+err.Error())
return
}
if len(f.RowIds) == 0 {
response.Fail(c, 101, response.TranslateMsg(c, "ParamsError"))
return
}
err := service.AllService.PeerService.BatchDelete(f.RowIds)
if err != nil {
response.Fail(c, 101, response.TranslateMsg(c, "OperationFailed")+err.Error())
return
}
response.Success(c, nil)
}

View File

@@ -9,9 +9,9 @@ import (
type Rustdesk struct { type Rustdesk struct {
} }
// ServerConfig 服务配置 // ServerConfig RUSTDESK服务配置
// @Tags ADMIN // @Tags ADMIN
// @Summary 服务配置 // @Summary RUSTDESK服务配置
// @Description 服务配置,给webclient提供api-server // @Description 服务配置,给webclient提供api-server
// @Accept json // @Accept json
// @Produce json // @Produce json
@@ -28,3 +28,19 @@ func (r *Rustdesk) ServerConfig(c *gin.Context) {
} }
response.Success(c, cf) response.Success(c, cf)
} }
// AppConfig APP服务配置
// @Tags ADMIN
// @Summary APP服务配置
// @Description APP服务配置
// @Accept json
// @Produce json
// @Success 200 {object} response.Response
// @Failure 500 {object} response.Response
// @Router /admin/app-config [get]
// @Security token
func (r *Rustdesk) AppConfig(c *gin.Context) {
response.Success(c, &gin.H{
"web_client": global.Config.App.WebClient,
})
}

View File

@@ -8,7 +8,6 @@ import (
"Gwen/model" "Gwen/model"
"Gwen/service" "Gwen/service"
"encoding/json" "encoding/json"
"fmt"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"net/http" "net/http"
"strconv" "strconv"
@@ -119,9 +118,10 @@ func (a *Ab) Tags(c *gin.Context) {
// @Description 标签 // @Description 标签
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Param guid path string true "guid"
// @Success 200 {string} string // @Success 200 {string} string
// @Failure 500 {object} response.ErrorResponse // @Failure 500 {object} response.ErrorResponse
// @Router /ab/add [post] // @Router /ab/tag/add/{guid} [post]
// @Security BearerAuth // @Security BearerAuth
func (a *Ab) TagAdd(c *gin.Context) { func (a *Ab) TagAdd(c *gin.Context) {
t := &model.Tag{} t := &model.Tag{}
@@ -151,6 +151,7 @@ func (a *Ab) TagAdd(c *gin.Context) {
// @Description 标签 // @Description 标签
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Param guid path string true "guid"
// @Success 200 {string} string // @Success 200 {string} string
// @Failure 500 {object} response.ErrorResponse // @Failure 500 {object} response.ErrorResponse
// @Router /ab/tag/rename/{guid} [put] // @Router /ab/tag/rename/{guid} [put]
@@ -188,6 +189,7 @@ func (a *Ab) TagRename(c *gin.Context) {
// @Description 标签 // @Description 标签
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Param guid path string true "guid"
// @Success 200 {string} string // @Success 200 {string} string
// @Failure 500 {object} response.ErrorResponse // @Failure 500 {object} response.ErrorResponse
// @Router /ab/tag/update/{guid} [put] // @Router /ab/tag/update/{guid} [put]
@@ -220,6 +222,7 @@ func (a *Ab) TagUpdate(c *gin.Context) {
// @Description 标签 // @Description 标签
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Param guid path string true "guid"
// @Success 200 {string} string // @Success 200 {string} string
// @Failure 500 {object} response.ErrorResponse // @Failure 500 {object} response.ErrorResponse
// @Router /ab/tag/{guid} [delete] // @Router /ab/tag/{guid} [delete]
@@ -274,7 +277,7 @@ func (a *Ab) Personal(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
"guid": guid, "guid": guid,
"name": user.Username, "name": user.Username,
"rule": 0, "rule": 3,
}) })
} else { } else {
c.JSON(http.StatusOK, nil) c.JSON(http.StatusOK, nil)
@@ -305,7 +308,8 @@ func (a *Ab) Settings(c *gin.Context) {
// @Description 共享 // @Description 共享
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Param string body string false "string valid" // @Param current query int false "页码"
// @Param pageSize query int false "每页数量"
// @Success 200 {object} response.Response // @Success 200 {object} response.Response
// @Failure 500 {object} response.Response // @Failure 500 {object} response.Response
// @Router /ab/shared/profiles [post] // @Router /ab/shared/profiles [post]
@@ -323,14 +327,14 @@ func (a *Ab) SharedProfiles(c *gin.Context) {
"name": "admin", "name": "admin",
"owner": "admin", "owner": "admin",
"note": "admin11", "note": "admin11",
"rule": 0, "rule": 3,
} }
item2 := map[string]interface{}{ item2 := map[string]interface{}{
"guid": "2", "guid": "2",
"name": "admin2", "name": "admin2",
"owner": "admin2", "owner": "admin2",
"note": "admin22", "note": "admin22",
"rule": 0, "rule": 2,
} }
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
"total": 2, "total": 2,
@@ -349,7 +353,9 @@ func (a *Ab) SharedProfiles(c *gin.Context) {
// @Description 地址 // @Description 地址
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Param string body string false "string valid" // @Param current query int false "页码"
// @Param pageSize query int false "每页数量"
// @Param ab query string false "guid"
// @Success 200 {object} response.Response // @Success 200 {object} response.Response
// @Failure 500 {object} response.Response // @Failure 500 {object} response.Response
// @Router /ab/peers [post] // @Router /ab/peers [post]
@@ -370,7 +376,7 @@ func (a *Ab) Peers(c *gin.Context) {
// @Description 标签 // @Description 标签
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Param id path string true "id" // @Param guid path string true "guid"
// @Success 200 {object} model.TagList // @Success 200 {object} model.TagList
// @Failure 500 {object} response.ErrorResponse // @Failure 500 {object} response.ErrorResponse
// @Router /ab/tags/{guid} [post] // @Router /ab/tags/{guid} [post]
@@ -388,24 +394,35 @@ func (a *Ab) PTags(c *gin.Context) {
// @Description 添加地址 // @Description 添加地址
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Param id path string true "id" // @Param guid path string true "guid"
// @Success 200 {string} string // @Success 200 {string} string
// @Failure 500 {object} response.ErrorResponse // @Failure 500 {object} response.ErrorResponse
// @Router /ab/peer/add/{guid} [post] // @Router /ab/peer/add/{guid} [post]
// @Security BearerAuth // @Security BearerAuth
func (a *Ab) PeerAdd(c *gin.Context) { func (a *Ab) PeerAdd(c *gin.Context) {
// forceAlwaysRelay永远是字符串"false",真是坑 // forceAlwaysRelay永远是字符串"false"
//f := &gin.H{} //f := &gin.H{}
//guid := c.Param("guid")
f := &requstform.PersonalAddressBookForm{} f := &requstform.PersonalAddressBookForm{}
err := c.ShouldBindJSON(f) err := c.ShouldBindJSON(f)
if err != nil { if err != nil {
response.Error(c, response.TranslateMsg(c, "ParamsError")+err.Error()) response.Error(c, response.TranslateMsg(c, "ParamsError")+err.Error())
return return
} }
fmt.Println(f) //fmt.Println(f)
u := service.AllService.UserService.CurUser(c) u := service.AllService.UserService.CurUser(c)
f.UserId = u.Id f.UserId = u.Id
ab := f.ToAddressBook() ab := f.ToAddressBook()
if ab.Platform == "" || ab.Username == "" || ab.Hostname == "" {
peer := service.AllService.PeerService.FindById(ab.Id)
if peer.RowId != 0 {
ab.Platform = service.AllService.AddressBookService.PlatformFromOs(peer.Os)
ab.Username = peer.Username
ab.Hostname = peer.Hostname
}
}
err = service.AllService.AddressBookService.AddAddressBook(ab) err = service.AllService.AddressBookService.AddAddressBook(ab)
if err != nil { if err != nil {
response.Error(c, response.TranslateMsg(c, "OperationFailed")+err.Error()) response.Error(c, response.TranslateMsg(c, "OperationFailed")+err.Error())
@@ -420,7 +437,7 @@ func (a *Ab) PeerAdd(c *gin.Context) {
// @Description 删除地址 // @Description 删除地址
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Param id path string true "id" // @Param guid path string true "guid"
// @Success 200 {string} string // @Success 200 {string} string
// @Failure 500 {object} response.ErrorResponse // @Failure 500 {object} response.ErrorResponse
// @Router /ab/peer/add/{guid} [delete] // @Router /ab/peer/add/{guid} [delete]
@@ -455,7 +472,7 @@ func (a *Ab) PeerDel(c *gin.Context) {
// @Description 更新地址 // @Description 更新地址
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Param id path string true "id" // @Param guid path string true "guid"
// @Success 200 {string} string // @Success 200 {string} string
// @Failure 500 {object} response.ErrorResponse // @Failure 500 {object} response.ErrorResponse
// @Router /ab/peer/update/{guid} [put] // @Router /ab/peer/update/{guid} [put]

View File

@@ -0,0 +1,56 @@
package api
import (
request "Gwen/http/request/api"
"Gwen/http/response"
"Gwen/model"
"Gwen/service"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
"time"
)
type Audit struct {
}
// AuditConn
// @Tags 审计
// @Summary 审计连接
// @Description 审计连接
// @Accept json
// @Produce json
// @Param body body request.AuditConnForm true "审计连接"
// @Success 200 {string} string ""
// @Failure 500 {object} response.Response
// @Router /audit/conn [post]
func (a *Audit) AuditConn(c *gin.Context) {
af := &request.AuditConnForm{}
err := c.ShouldBindBodyWith(af, binding.JSON)
if err != nil {
response.Error(c, response.TranslateMsg(c, "ParamsError")+err.Error())
return
}
//fmt.Println(af)
ac := af.ToAuditConn()
if af.Action == model.AuditActionNew {
service.AllService.AuditService.CreateAuditConn(ac)
} else if af.Action == model.AuditActionClose {
ex := service.AllService.AuditService.InfoByPeerIdAndConnId(af.Id, af.ConnId)
if ex.Id != 0 {
ex.CloseTime = time.Now().Unix()
service.AllService.AuditService.UpdateAuditConn(ex)
}
} else if af.Action == "" {
ex := service.AllService.AuditService.InfoByPeerIdAndConnId(af.Id, af.ConnId)
if ex.Id != 0 {
up := &model.AuditConn{
IdModel: model.IdModel{Id: ex.Id},
FromPeer: ac.FromPeer,
FromName: ac.FromName,
SessionId: ac.SessionId,
}
service.AllService.AuditService.UpdateAuditConn(up)
}
}
response.Success(c, "")
}

View File

@@ -1,9 +1,13 @@
package api package api
import ( import (
requstform "Gwen/http/request/api"
"Gwen/http/response" "Gwen/http/response"
"Gwen/model"
"Gwen/service"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"net/http" "net/http"
"time"
) )
type Index struct { type Index struct {
@@ -35,10 +39,26 @@ func (i *Index) Index(c *gin.Context) {
// @Failure 500 {object} response.Response // @Failure 500 {object} response.Response
// @Router /heartbeat [post] // @Router /heartbeat [post]
func (i *Index) Heartbeat(c *gin.Context) { func (i *Index) Heartbeat(c *gin.Context) {
//b := &gin.H{} info := &requstform.PeerInfoInHeartbeat{}
//err := c.BindJSON(b) err := c.ShouldBindJSON(info)
//body : &map[id:xxx modified_at:0 uuid:NGIxZTZjM2YtNmNkMy00YTMwLWFiNjQtMzQ0MTA0NGE5ZDgz ver:1.003e+06] if err != nil {
//fmt.Println(b, err, c.Request.Header) c.JSON(http.StatusOK, gin.H{})
//header : map[Accept:[*/*] Accept-Encoding:[gzip] Content-Length:[105] Content-Type:[application/json]] return
}
if info.Uuid == "" {
c.JSON(http.StatusOK, gin.H{})
return
}
peer := service.AllService.PeerService.FindByUuid(info.Uuid)
if peer == nil {
c.JSON(http.StatusOK, gin.H{})
return
}
//如果在一分钟以内则不更新
if time.Now().Unix()-peer.LastOnlineTime > 60 {
peer.LastOnlineTime = time.Now().Unix()
upp := &model.Peer{RowId: peer.RowId, LastOnlineTime: peer.LastOnlineTime}
service.AllService.PeerService.Update(upp)
}
c.JSON(http.StatusOK, gin.H{}) c.JSON(http.StatusOK, gin.H{})
} }

View File

@@ -8,6 +8,7 @@ import (
"Gwen/model" "Gwen/model"
"Gwen/service" "Gwen/service"
"encoding/json" "encoding/json"
"fmt"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"net/http" "net/http"
) )
@@ -30,12 +31,14 @@ func (l *Login) Login(c *gin.Context) {
err := c.ShouldBindJSON(f) err := c.ShouldBindJSON(f)
//fmt.Println(f) //fmt.Println(f)
if err != nil { if err != nil {
global.Logger.Warn(fmt.Sprintf("Login Fail: %s %s %s", "ParamsError", c.RemoteIP(), c.ClientIP()))
response.Error(c, response.TranslateMsg(c, "ParamsError")+err.Error()) response.Error(c, response.TranslateMsg(c, "ParamsError")+err.Error())
return return
} }
errList := global.Validator.ValidStruct(c, f) errList := global.Validator.ValidStruct(c, f)
if len(errList) > 0 { if len(errList) > 0 {
global.Logger.Warn(fmt.Sprintf("Login Fail: %s %s %s", "ParamsError", c.RemoteIP(), c.ClientIP()))
response.Error(c, errList[0]) response.Error(c, errList[0])
return return
} }
@@ -43,6 +46,7 @@ func (l *Login) Login(c *gin.Context) {
u := service.AllService.UserService.InfoByUsernamePassword(f.Username, f.Password) u := service.AllService.UserService.InfoByUsernamePassword(f.Username, f.Password)
if u.Id == 0 { if u.Id == 0 {
global.Logger.Warn(fmt.Sprintf("Login Fail: %s %s %s", "UsernameOrPasswordError", c.RemoteIP(), c.ClientIP()))
response.Error(c, response.TranslateMsg(c, "UsernameOrPasswordError")) response.Error(c, response.TranslateMsg(c, "UsernameOrPasswordError"))
return return
} }

View File

@@ -22,7 +22,6 @@ type Peer struct {
// @Success 200 {string} string "SYSINFO_UPDATED,ID_NOT_FOUND" // @Success 200 {string} string "SYSINFO_UPDATED,ID_NOT_FOUND"
// @Failure 500 {object} response.ErrorResponse // @Failure 500 {object} response.ErrorResponse
// @Router /sysinfo [post] // @Router /sysinfo [post]
// @Security BearerAuth
func (p *Peer) SysInfo(c *gin.Context) { func (p *Peer) SysInfo(c *gin.Context) {
f := &requstform.PeerForm{} f := &requstform.PeerForm{}
err := c.ShouldBindBodyWith(f, binding.JSON) err := c.ShouldBindBodyWith(f, binding.JSON)
@@ -30,19 +29,30 @@ func (p *Peer) SysInfo(c *gin.Context) {
response.Error(c, response.TranslateMsg(c, "ParamsError")+err.Error()) response.Error(c, response.TranslateMsg(c, "ParamsError")+err.Error())
return return
} }
fpe := f.ToPeer()
pe := service.AllService.PeerService.FindById(f.Id) pe := service.AllService.PeerService.FindById(f.Id)
if pe == nil || pe.RowId == 0 { if pe.RowId == 0 {
pe = f.ToPeer() pe = f.ToPeer()
pe.UserId = service.AllService.UserService.FindLatestUserIdFromLoginLogByUuid(pe.Uuid)
err = service.AllService.PeerService.Create(pe) err = service.AllService.PeerService.Create(pe)
if err != nil { if err != nil {
response.Error(c, response.TranslateMsg(c, "OperationFailed")+err.Error()) response.Error(c, response.TranslateMsg(c, "OperationFailed")+err.Error())
return return
} }
} else {
if pe.UserId == 0 {
pe.UserId = service.AllService.UserService.FindLatestUserIdFromLoginLogByUuid(pe.Uuid)
}
fpe.RowId = pe.RowId
fpe.UserId = pe.UserId
err = service.AllService.PeerService.Update(fpe)
if err != nil {
response.Error(c, response.TranslateMsg(c, "OperationFailed")+err.Error())
return
}
} }
//SYSINFO_UPDATED 上传成功 //SYSINFO_UPDATED 上传成功
//ID_NOT_FOUND 下次心跳会上传 //ID_NOT_FOUND 下次心跳会上传
//直接响应文本 //直接响应文本
c.String(http.StatusOK, "") c.String(http.StatusOK, "SYSINFO_UPDATED")
} }

View File

@@ -22,7 +22,7 @@ type User struct {
// @Security token // @Security token
//func (u *User) currentUser(c *gin.Context) { //func (u *User) currentUser(c *gin.Context) {
// user := service.AllService.UserService.CurUser(c) // user := service.AllService.UserService.CurUser(c)
// up := (&apiResp.UserPayload{}).FromUser(user) // up := (&apiResp.UserPayload{}).FromName(user)
// c.JSON(http.StatusOK, up) // c.JSON(http.StatusOK, up)
//} //}

View File

@@ -6,6 +6,7 @@ import (
"Gwen/http/response/api" "Gwen/http/response/api"
"Gwen/service" "Gwen/service"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"time"
) )
type WebClient struct { type WebClient struct {
@@ -40,3 +41,47 @@ func (i *WebClient) ServerConfig(c *gin.Context) {
}, },
) )
} }
// SharedPeer 分享的peer
// @Tags WEBCLIENT
// @Summary 分享的peer
// @Description 分享的peer
// @Accept json
// @Produce json
// @Success 200 {object} response.Response
// @Failure 500 {object} response.Response
// @Router /shared-peer [post]
func (i *WebClient) SharedPeer(c *gin.Context) {
j := &gin.H{}
c.ShouldBindJSON(j)
t := (*j)["share_token"].(string)
if t == "" {
response.Fail(c, 101, "share_token is required")
return
}
sr := service.AllService.AddressBookService.SharedPeer(t)
if sr == nil || sr.Id == 0 {
response.Fail(c, 101, "share not found")
return
}
//判断是否过期,created_at + expire > now
ca := time.Time(sr.CreatedAt)
if ca.Add(time.Second * time.Duration(sr.Expire)).Before(time.Now()) {
response.Fail(c, 101, "share expired")
return
}
ab := service.AllService.AddressBookService.InfoByUserIdAndId(sr.UserId, sr.PeerId)
if ab.RowId == 0 {
response.Fail(c, 101, "peer not found")
return
}
pp := &api.WebClientPeerPayload{}
pp.FromShareRecord(sr)
pp.Info.Username = ab.Username
pp.Info.Hostname = ab.Hostname
response.Success(c, gin.H{
"id_server": global.Config.Rustdesk.IdServer,
"key": global.Config.Rustdesk.Key,
"peer": pp,
})
}

View File

@@ -16,85 +16,7 @@ func (i *Index) ConfigJs(c *gin.Context) {
apiServer := global.Config.Rustdesk.ApiServer apiServer := global.Config.Rustdesk.ApiServer
tmp := ` tmp := `
function stringToUint8Array(str){
var arr = [];
for (var i = 0, j = str.length; i < j; ++i) {
arr.push(str.charCodeAt(i));
}
var tmpUint8Array = new Uint8Array(arr);
return tmpUint8Array
}
window._gwen = {}
window._gwen.kv = {}
function getQueryVariable() {
const query = window.location.hash.substring(3);
const vars = query.split("&");
for (var i = 0; i < vars.length; i++) {
var pair = vars[i].split("=");
window._gwen.kv[pair[0]] = pair[1]
}
}
getQueryVariable()
const id = window._gwen.kv.id || ''
if (id) {
localStorage.setItem('remote-id', id)
}
window._gwen.hosts = [
"rs-sg.rustdesk.com",
"rs-cn.rustdesk.com",
"rs-us.rustdesk.com",
]
localStorage.setItem('api-server', "` + apiServer + `") localStorage.setItem('api-server', "` + apiServer + `")
const autoWriteServer = () => {
return setTimeout(() => {
const token = localStorage.getItem('access_token')
const apiserver = localStorage.getItem('api-server')
if (token && apiserver) {
fetch(apiserver + "/api/server-config", {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + token
}
}
).then(res => res.json()).then(res => {
if (res.code === 0) {
if(!localStorage.getItem('custom-rendezvous-server') || !localStorage.getItem('key') ) {
localStorage.setItem('custom-rendezvous-server', res.data.id_server)
localStorage.setItem('key', res.data.key)
}
if (res.data.peers) {
oldPeers = JSON.parse(localStorage.getItem('peers')) || {}
let needUpdate = false
Object.keys(res.data.peers).forEach(k => {
if(!oldPeers[k]) {
oldPeers[k] = res.data.peers[k]
needUpdate = true
}else{
oldPeers[k].info = res.data.peers[k].info
}
if (oldPeers[k].info && oldPeers[k].info.hash&&!oldPeers[k].password ) {
let p1 = window.atob(oldPeers[k].info.hash)
const pwd = stringToUint8Array(p1)
oldPeers[k].password = pwd.toString()
oldPeers[k].remember = true
}
})
localStorage.setItem('peers', JSON.stringify(oldPeers))
if(needUpdate) {
window.location.reload()
}
}
}
})
} else {
autoWriteServer()
}
}, 1000)
}
autoWriteServer()
` `
c.String(200, tmp) c.String(200, tmp)
} }

View File

@@ -7,7 +7,7 @@ import (
func RustAuth() gin.HandlerFunc { func RustAuth() gin.HandlerFunc {
return func(c *gin.Context) { return func(c *gin.Context) {
//fmt.Println(c.Request.Header) //fmt.Println(c.Request.URL, c.Request.Header)
//获取HTTP_AUTHORIZATION //获取HTTP_AUTHORIZATION
token := c.GetHeader("Authorization") token := c.GetHeader("Authorization")
if token == "" { if token == "" {

View File

@@ -16,6 +16,7 @@ type AddressBookForm struct {
Tags []string `json:"tags"` Tags []string `json:"tags"`
Hash string `json:"hash"` Hash string `json:"hash"`
UserId uint `json:"user_id"` UserId uint `json:"user_id"`
UserIds []uint `json:"user_ids"`
ForceAlwaysRelay bool `json:"forceAlwaysRelay"` ForceAlwaysRelay bool `json:"forceAlwaysRelay"`
RdpPort string `json:"rdpPort"` RdpPort string `json:"rdpPort"`
RdpUsername string `json:"rdpUsername"` RdpUsername string `json:"rdpUsername"`
@@ -48,9 +49,56 @@ func (a AddressBookForm) ToAddressBook() *model.AddressBook {
} }
} }
func (a AddressBookForm) ToAddressBooks() []*model.AddressBook {
//tags转换
tags, _ := json.Marshal(a.Tags)
abs := make([]*model.AddressBook, 0, len(a.UserIds))
for _, userId := range a.UserIds {
abs = append(abs, &model.AddressBook{
RowId: a.RowId,
Id: a.Id,
Username: a.Username,
Password: a.Password,
Hostname: a.Hostname,
Alias: a.Alias,
Platform: a.Platform,
Tags: tags,
Hash: a.Hash,
UserId: userId,
ForceAlwaysRelay: a.ForceAlwaysRelay,
RdpPort: a.RdpPort,
RdpUsername: a.RdpUsername,
Online: a.Online,
LoginName: a.LoginName,
SameServer: a.SameServer,
})
}
return abs
}
type AddressBookQuery struct { type AddressBookQuery struct {
UserId int `form:"user_id"` UserId int `form:"user_id"`
IsMy int `form:"is_my"` IsMy int `form:"is_my"`
Username string `form:"username"`
Hostname string `form:"hostname"`
Id string `form:"id"`
PageQuery PageQuery
} }
type ShareByWebClientForm struct {
Id string `json:"id" validate:"required"`
PasswordType string `json:"password_type" validate:"required,oneof=once fixed"` //只能是once,fixed
Password string `json:"password" validate:"required"`
Expire int64 `json:"expire"`
}
func (sbwcf ShareByWebClientForm) ToShareRecord() *model.ShareRecord {
return &model.ShareRecord{
UserId: 0,
PeerId: sbwcf.Id,
PasswordType: sbwcf.PasswordType,
Password: sbwcf.Password,
Expire: sbwcf.Expire,
}
}

View File

@@ -0,0 +1,7 @@
package admin
type AuditQuery struct {
PeerId string `form:"peer_id"`
FromPeer string `form:"from_peer"`
PageQuery
}

View File

@@ -5,11 +5,13 @@ import "Gwen/model"
type GroupForm struct { type GroupForm struct {
Id uint `json:"id"` Id uint `json:"id"`
Name string `json:"name" validate:"required"` Name string `json:"name" validate:"required"`
Type int `json:"type"`
} }
func (gf *GroupForm) FromGroup(group *model.Group) *GroupForm { func (gf *GroupForm) FromGroup(group *model.Group) *GroupForm {
gf.Id = group.Id gf.Id = group.Id
gf.Name = group.Name gf.Name = group.Name
gf.Type = group.Type
return gf return gf
} }
@@ -17,5 +19,6 @@ func (gf *GroupForm) ToGroup() *model.Group {
group := &model.Group{} group := &model.Group{}
group.Id = gf.Id group.Id = gf.Id
group.Name = gf.Name group.Name = gf.Name
group.Type = gf.Type
return group return group
} }

View File

@@ -14,6 +14,10 @@ type PeerForm struct {
Version string `json:"version"` Version string `json:"version"`
} }
type PeerBatchDeleteForm struct {
RowIds []uint `json:"row_ids" validate:"required"`
}
// ToPeer // ToPeer
func (f *PeerForm) ToPeer() *model.Peer { func (f *PeerForm) ToPeer() *model.Peer {
return &model.Peer{ return &model.Peer{
@@ -28,3 +32,10 @@ func (f *PeerForm) ToPeer() *model.Peer {
Version: f.Version, Version: f.Version,
} }
} }
type PeerQuery struct {
PageQuery
TimeAgo int `json:"time_ago" form:"time_ago"`
Id string `json:"id" form:"id"`
Hostname string `json:"hostname" form:"hostname"`
}

40
http/request/api/audit.go Normal file
View File

@@ -0,0 +1,40 @@
package api
import (
"Gwen/model"
"strconv"
)
type AuditConnForm struct {
Action string `json:"action"`
ConnId int64 `json:"conn_id"`
Id string `json:"id"`
Peer []string `json:"peer"`
Ip string `json:"ip"`
SessionId float64 `json:"session_id"`
Type int `json:"type"`
Uuid string `json:"uuid"`
}
func (a *AuditConnForm) ToAuditConn() *model.AuditConn {
fp := ""
fn := ""
if len(a.Peer) >= 1 {
fp = a.Peer[0]
if len(a.Peer) == 2 {
fn = a.Peer[1]
}
}
ssid := strconv.FormatFloat(a.SessionId, 'f', -1, 64)
return &model.AuditConn{
Action: a.Action,
ConnId: a.ConnId,
PeerId: a.Id,
FromPeer: fp,
FromName: fn,
Ip: a.Ip,
SessionId: ssid,
Type: a.Type,
Uuid: a.Uuid,
}
}

View File

@@ -71,3 +71,9 @@ type TagColorForm struct {
Name string `json:"name"` Name string `json:"name"`
Color uint `json:"color"` Color uint `json:"color"`
} }
type PeerInfoInHeartbeat struct {
Id string `json:"id"`
Uuid string `json:"uuid"`
Ver int `json:"ver"`
}

View File

@@ -9,6 +9,7 @@ type WebClientPeerPayload struct {
ViewStyle string `json:"view-style"` ViewStyle string `json:"view-style"`
Tm int64 `json:"tm"` Tm int64 `json:"tm"`
Info WebClientPeerInfoPayload `json:"info"` Info WebClientPeerInfoPayload `json:"info"`
Tmppwd string `json:"tmppwd"`
} }
type WebClientPeerInfoPayload struct { type WebClientPeerInfoPayload struct {
@@ -16,6 +17,7 @@ type WebClientPeerInfoPayload struct {
Hostname string `json:"hostname"` Hostname string `json:"hostname"`
Platform string `json:"platform"` Platform string `json:"platform"`
Hash string `json:"hash"` Hash string `json:"hash"`
Id string `json:"id"`
} }
func (wcpp *WebClientPeerPayload) FromAddressBook(a *model.AddressBook) { func (wcpp *WebClientPeerPayload) FromAddressBook(a *model.AddressBook) {
@@ -29,3 +31,16 @@ func (wcpp *WebClientPeerPayload) FromAddressBook(a *model.AddressBook) {
Hash: a.Hash, Hash: a.Hash,
} }
} }
func (wcpp *WebClientPeerPayload) FromShareRecord(sr *model.ShareRecord) {
wcpp.ViewStyle = "shrink"
//24小时前
wcpp.Tm = time.Now().UnixNano()
wcpp.Tmppwd = sr.Password
wcpp.Info = WebClientPeerInfoPayload{
Username: "",
Hostname: "",
Platform: "",
Id: sr.PeerId,
}
}

View File

@@ -27,9 +27,10 @@ func Init(g *gin.Engine) {
PeerBind(adg) PeerBind(adg)
OauthBind(adg) OauthBind(adg)
LoginLogBind(adg) LoginLogBind(adg)
AuditBind(adg)
rs := &admin.Rustdesk{} rs := &admin.Rustdesk{}
adg.GET("/server-config", rs.ServerConfig) adg.GET("/server-config", rs.ServerConfig)
adg.GET("/app-config", rs.AppConfig)
//访问静态文件 //访问静态文件
//g.StaticFS("/upload", http.Dir(global.Config.Gin.ResourcesPath+"/upload")) //g.StaticFS("/upload", http.Dir(global.Config.Gin.ResourcesPath+"/upload"))
@@ -93,6 +94,10 @@ func AddressBookBind(rg *gin.RouterGroup) {
aR.POST("/create", cont.Create) aR.POST("/create", cont.Create)
aR.POST("/update", cont.Update) aR.POST("/update", cont.Update)
aR.POST("/delete", cont.Delete) aR.POST("/delete", cont.Delete)
aR.POST("/shareByWebClient", cont.ShareByWebClient)
arp := aR.Use(middleware.AdminPrivilege())
arp.POST("/batchCreate", cont.BatchCreate)
} }
} }
func PeerBind(rg *gin.RouterGroup) { func PeerBind(rg *gin.RouterGroup) {
@@ -104,6 +109,9 @@ func PeerBind(rg *gin.RouterGroup) {
aR.POST("/create", cont.Create) aR.POST("/create", cont.Create)
aR.POST("/update", cont.Update) aR.POST("/update", cont.Update)
aR.POST("/delete", cont.Delete) aR.POST("/delete", cont.Delete)
arp := aR.Use(middleware.AdminPrivilege())
arp.POST("/batchDelete", cont.BatchDelete)
} }
} }
@@ -135,6 +143,12 @@ func LoginLogBind(rg *gin.RouterGroup) {
aR.GET("/list", cont.List) aR.GET("/list", cont.List)
aR.POST("/delete", cont.Delete) aR.POST("/delete", cont.Delete)
} }
func AuditBind(rg *gin.RouterGroup) {
cont := &admin.Audit{}
aR := rg.Group("/audit_conn").Use(middleware.AdminPrivilege())
aR.GET("/list", cont.ConnList)
aR.POST("/delete", cont.ConnDelete)
}
/* /*
func FileBind(rg *gin.RouterGroup) { func FileBind(rg *gin.RouterGroup) {

View File

@@ -46,12 +46,14 @@ func ApiInit(g *gin.Engine) {
//提交系统信息 //提交系统信息
frg.POST("/sysinfo", pe.SysInfo) frg.POST("/sysinfo", pe.SysInfo)
} }
frg.Use(middleware.RustAuth())
{
w := &api.WebClient{}
frg.POST("/server-config", w.ServerConfig)
}
if global.Config.App.WebClient == 1 {
WebClientRoutes(frg)
}
au := &api.Audit{}
//[method:POST] [uri:/api/audit/conn]
frg.POST("/audit/conn", au.AuditConn)
frg.Use(middleware.RustAuth())
{ {
u := &api.User{} u := &api.User{}
frg.GET("/user/info", u.Info) frg.GET("/user/info", u.Info)
@@ -109,3 +111,14 @@ func PersonalRoutes(frg *gin.RouterGroup) {
} }
} }
func WebClientRoutes(frg *gin.RouterGroup) {
w := &api.WebClient{}
{
frg.POST("/shared-peer", w.SharedPeer)
}
{
frg.POST("/server-config", middleware.RustAuth(), w.ServerConfig)
}
}

View File

@@ -10,7 +10,13 @@ import (
func WebInit(g *gin.Engine) { func WebInit(g *gin.Engine) {
i := &web.Index{} i := &web.Index{}
g.GET("/", i.Index) g.GET("/", i.Index)
g.GET("/webclient-config/index.js", i.ConfigJs)
g.StaticFS("/webclient", http.Dir(global.Config.Gin.ResourcesPath+"/web")) if global.Config.App.WebClient == 1 {
g.GET("/webclient-config/index.js", i.ConfigJs)
}
if global.Config.App.WebClient == 1 {
g.StaticFS("/webclient", http.Dir(global.Config.Gin.ResourcesPath+"/web"))
}
g.StaticFS("/_admin", http.Dir(global.Config.Gin.ResourcesPath+"/admin")) g.StaticFS("/_admin", http.Dir(global.Config.Gin.ResourcesPath+"/admin"))
} }

View File

@@ -1,9 +1,12 @@
package orm package orm
import ( import (
"Gwen/global"
"fmt" "fmt"
"gorm.io/driver/mysql" "gorm.io/driver/mysql"
"gorm.io/gorm" "gorm.io/gorm"
"gorm.io/gorm/logger"
"time"
) )
type MysqlConfig struct { type MysqlConfig struct {
@@ -22,6 +25,16 @@ func NewMysql(mysqlConf *MysqlConfig) *gorm.DB {
//SkipInitializeWithVersion: false, // 根据当前 MySQL 版本自动配置 //SkipInitializeWithVersion: false, // 根据当前 MySQL 版本自动配置
}), &gorm.Config{ }), &gorm.Config{
DisableForeignKeyConstraintWhenMigrating: true, DisableForeignKeyConstraintWhenMigrating: true,
Logger: logger.New(
global.Logger, // io writer
logger.Config{
SlowThreshold: time.Second, // Slow SQL threshold
LogLevel: logger.Warn, // Log level
IgnoreRecordNotFoundError: true, // Ignore ErrRecordNotFound error for logger
ParameterizedQueries: true, // Don't include params in the SQL log
Colorful: true,
},
),
}) })
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)

View File

@@ -1,9 +1,12 @@
package orm package orm
import ( import (
"Gwen/global"
"fmt" "fmt"
"gorm.io/driver/sqlite" "gorm.io/driver/sqlite"
"gorm.io/gorm" "gorm.io/gorm"
"gorm.io/gorm/logger"
"time"
) )
type SqliteConfig struct { type SqliteConfig struct {
@@ -12,7 +15,19 @@ type SqliteConfig struct {
} }
func NewSqlite(sqliteConf *SqliteConfig) *gorm.DB { func NewSqlite(sqliteConf *SqliteConfig) *gorm.DB {
db, err := gorm.Open(sqlite.Open("./data/rustdeskapi.db"), &gorm.Config{}) db, err := gorm.Open(sqlite.Open("./data/rustdeskapi.db"), &gorm.Config{
DisableForeignKeyConstraintWhenMigrating: true,
Logger: logger.New(
global.Logger, // io writer
logger.Config{
SlowThreshold: time.Second, // Slow SQL threshold
LogLevel: logger.Warn, // Log level
IgnoreRecordNotFoundError: true, // Ignore ErrRecordNotFound error for logger
ParameterizedQueries: true, // Don't include params in the SQL log
Colorful: true,
},
),
})
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
} }

26
model/audit.go Normal file
View File

@@ -0,0 +1,26 @@
package model
const (
AuditActionNew = "new"
AuditActionClose = "close"
)
type AuditConn struct {
IdModel
Action string `json:"action" gorm:"default:'';not null;"`
ConnId int64 `json:"conn_id" gorm:"default:0;not null;index"`
PeerId string `json:"peer_id" gorm:"default:'';not null;index"`
FromPeer string `json:"from_peer" gorm:"default:'';not null;"`
FromName string `json:"from_name" gorm:"default:'';not null;"`
Ip string `json:"ip" gorm:"default:'';not null;"`
SessionId string `json:"session_id" gorm:"default:'';not null;"`
Type int `json:"type" gorm:"default:0;not null;"`
Uuid string `json:"uuid" gorm:"default:'';not null;"`
CloseTime int64 `json:"close_time" gorm:"default:0;not null;"`
TimeModel
}
type AuditConnList struct {
AuditConns []*AuditConn `json:"list"`
Pagination
}

View File

@@ -1,17 +1,18 @@
package model package model
type Peer struct { type Peer struct {
RowId uint `json:"row_id" gorm:"primaryKey;"` RowId uint `json:"row_id" gorm:"primaryKey;"`
Id string `json:"id" gorm:"default:'';not null;index"` Id string `json:"id" gorm:"default:'';not null;index"`
Cpu string `json:"cpu" gorm:"default:'';not null;"` Cpu string `json:"cpu" gorm:"default:'';not null;"`
Hostname string `json:"hostname" gorm:"default:'';not null;"` Hostname string `json:"hostname" gorm:"default:'';not null;"`
Memory string `json:"memory" gorm:"default:'';not null;"` Memory string `json:"memory" gorm:"default:'';not null;"`
Os string `json:"os" gorm:"default:'';not null;"` Os string `json:"os" gorm:"default:'';not null;"`
Username string `json:"username" gorm:"default:'';not null;"` Username string `json:"username" gorm:"default:'';not null;"`
Uuid string `json:"uuid" gorm:"default:'';not null;index"` Uuid string `json:"uuid" gorm:"default:'';not null;index"`
Version string `json:"version" gorm:"default:'';not null;"` Version string `json:"version" gorm:"default:'';not null;"`
UserId uint `json:"user_id" gorm:"default:0;not null;index"` UserId uint `json:"user_id" gorm:"default:0;not null;index"`
User User `json:"user,omitempty" gorm:""` User User `json:"user,omitempty" gorm:""`
LastOnlineTime int64 `json:"last_online_time" gorm:"default:0;not null;"`
TimeModel TimeModel
} }

12
model/shareRecord.go Normal file
View File

@@ -0,0 +1,12 @@
package model
type ShareRecord struct {
IdModel
UserId uint `json:"user_id" gorm:"default:0;not null;index"`
PeerId string `json:"peer_id" gorm:"default:'';not null;index"`
ShareToken string `json:"share_token" gorm:"default:'';not null;index"`
PasswordType string `json:"password_type" gorm:"default:'';not null;"`
Password string `json:"password" gorm:"default:'';not null;"`
Expire int64 `json:"expire" gorm:"default:0;not null;"`
TimeModel
}

View File

@@ -109,3 +109,13 @@ other = "Decode oauth user info error."
description = "Old password error." description = "Old password error."
one = "Old password error." one = "Old password error."
other = "Old password error." other = "Old password error."
[DefaultGroup]
description = "Default group"
one = "Default Group"
other = "Default Group"
[ShareGroup]
description = "Share group"
one = "Share Group"
other = "Share Group"

View File

@@ -109,4 +109,15 @@ other = "解析授权用户信息失败。"
[OldPasswordError] [OldPasswordError]
description = "Old password error." description = "Old password error."
one = "旧密码错误。" one = "旧密码错误。"
other = "旧密码错误。" other = "旧密码错误。"
[DefaultGroup]
description = "Default group."
one = "默认组"
other = "默认组"
[ShareGroup]
description = "Share group."
one = "共享组"
other = "共享组"

View File

@@ -2,188 +2,188 @@
<html> <html>
<head> <head>
<!-- <!--
If you are serving your web app in a path other than the root, change the If you are serving your web app in a path other than the root, change the
href value below to reflect the base path you are serving from. href value below to reflect the base path you are serving from.
The path provided below has to start and end with a slash "/" in order for The path provided below has to start and end with a slash "/" in order for
it to work correctly. it to work correctly.
For more details: For more details:
* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base * https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base
This is a placeholder for base href that will be replaced by the value of This is a placeholder for base href that will be replaced by the value of
the `--base-href` argument provided to `flutter build`. the `--base-href` argument provided to `flutter build`.
--> -->
<base href="/webclient/"> <base href="/webclient/">
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta content="IE=Edge" http-equiv="X-UA-Compatible"> <meta content="IE=Edge" http-equiv="X-UA-Compatible">
<meta name="description" content="Remote Desktop."> <meta name="description" content="Remote Desktop.">
<!-- iOS meta tags & icons --> <!-- iOS meta tags & icons -->
<meta name="apple-mobile-web-app-capable" content="yes"> <meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black"> <meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="RustDesk"> <meta name="apple-mobile-web-app-title" content="RustDesk">
<link rel="apple-touch-icon" href="icons/Icon-192.png"> <link rel="apple-touch-icon" href="icons/Icon-192.png">
<!-- Favicon --> <!-- Favicon -->
<link rel="icon" type="image/svg+xml" href="favicon.svg" /> <link rel="icon" type="image/svg+xml" href="favicon.svg"/>
<title>RustDesk</title> <title>RustDesk</title>
<link rel="manifest" href="manifest.json"> <link rel="manifest" href="manifest.json">
<script src="/webclient-config/index.js"></script> <script src="/webclient-config/index.js"></script>
<script> <script src="ogvjs-1.8.6/ogv.js"></script>
<script type="module" crossorigin src="js/dist/index.js"></script>
<script>
</script>
<link rel="modulepreload" href="js/dist/vendor.js">
<script src="yuv-canvas-1.2.6.js"></script>
<style>
.loading {
display: flex;
justify-content: center;
align-items: center;
margin: 0;
position: absolute;
top: 50%;
left: 50%;
-ms-transform: translate(-50%, -50%);
transform: translate(-50%, -50%);
}
</script> .loader {
<script src="ogvjs-1.8.6/ogv.js"></script> border: 16px solid #f3f3f3;
<script type="module" crossorigin src="js/dist/index.js"></script> border-radius: 50%;
<link rel="modulepreload" href="js/dist/vendor.js"> border: 15px solid;
<script src="yuv-canvas-1.2.6.js"></script> border-top: 16px solid #024eff;
border-right: 16px solid white;
border-bottom: 16px solid #024eff;
border-left: 16px solid white;
width: 120px;
height: 120px;
-webkit-animation: spin 2s linear infinite;
animation: spin 2s linear infinite;
}
<style> @-webkit-keyframes spin {
.loading { 0% {
display: flex; -webkit-transform: rotate(0deg);
justify-content: center; }
align-items: center;
margin: 0;
position: absolute;
top: 50%;
left: 50%;
-ms-transform: translate(-50%, -50%);
transform: translate(-50%, -50%);
}
.loader { 100% {
border: 16px solid #f3f3f3; -webkit-transform: rotate(360deg);
border-radius: 50%; }
border: 15px solid; }
border-top: 16px solid #024eff;
border-right: 16px solid white;
border-bottom: 16px solid #024eff;
border-left: 16px solid white;
width: 120px;
height: 120px;
-webkit-animation: spin 2s linear infinite;
animation: spin 2s linear infinite;
}
@-webkit-keyframes spin { @keyframes spin {
0% { 0% {
-webkit-transform: rotate(0deg); transform: rotate(0deg);
} }
100% { 100% {
-webkit-transform: rotate(360deg); transform: rotate(360deg);
} }
} }
</style>
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
</style>
</head> </head>
<body> <body>
<div class="loading"> <div class="loading">
<div class="loader"></div> <div class="loader"></div>
</div> </div>
<!-- This script installs service_worker.js to provide PWA functionality to <!-- This script installs service_worker.js to provide PWA functionality to
application. For more information, see: application. For more information, see:
https://developers.google.com/web/fundamentals/primers/service-workers --> https://developers.google.com/web/fundamentals/primers/service-workers -->
<script> <script>
var serviceWorkerVersion = '1200232272'; var serviceWorkerVersion = '1200232272';
var scriptLoaded = false; var scriptLoaded = false;
function loadMainDartJs() { function loadMainDartJs() {
if (scriptLoaded) { if (scriptLoaded) {
return; return;
} }
scriptLoaded = true; scriptLoaded = true;
var scriptTag = document.createElement('script'); var scriptTag = document.createElement('script');
scriptTag.src = 'main.dart.js'; scriptTag.src = 'main.dart.js';
scriptTag.type = 'application/javascript'; scriptTag.type = 'application/javascript';
document.body.append(scriptTag); document.body.append(scriptTag);
} }
if ('serviceWorker' in navigator) { if ('serviceWorker' in navigator) {
// Service workers are supported. Use them. // Service workers are supported. Use them.
window.addEventListener('load', function () { window.addEventListener('load', function () {
// Wait for registration to finish before dropping the <script> tag. // Wait for registration to finish before dropping the <script> tag.
// Otherwise, the browser will load the script multiple times, // Otherwise, the browser will load the script multiple times,
// potentially different versions. // potentially different versions.
var serviceWorkerUrl = 'flutter_service_worker.js?v=' + serviceWorkerVersion; var serviceWorkerUrl = 'flutter_service_worker.js?v=' + serviceWorkerVersion;
navigator.serviceWorker.register(serviceWorkerUrl) navigator.serviceWorker.register(serviceWorkerUrl)
.then((reg) => { .then((reg) => {
function waitForActivation(serviceWorker) { function waitForActivation(serviceWorker) {
serviceWorker.addEventListener('statechange', () => { serviceWorker.addEventListener('statechange', () => {
if (serviceWorker.state == 'activated') { if (serviceWorker.state == 'activated') {
console.log('Installed new service worker.'); console.log('Installed new service worker.');
loadMainDartJs(); loadMainDartJs();
}
});
}
if (!reg.active && (reg.installing || reg.waiting)) {
// No active web worker and we have installed or are installing
// one for the first time. Simply wait for it to activate.
waitForActivation(reg.installing || reg.waiting);
} else if (!reg.active.scriptURL.endsWith(serviceWorkerVersion)) {
// When the app updates the serviceWorkerVersion changes, so we
// need to ask the service worker to update.
console.log('New service worker available.');
reg.update();
waitForActivation(reg.installing);
} else {
// Existing service worker is still good.
console.log('Loading app from service worker.');
loadMainDartJs();
}
});
// If service worker doesn't succeed in a reasonable amount of time,
// fallback to plaint <script> tag.
setTimeout(() => {
if (!scriptLoaded) {
console.warn(
'Failed to load app from service worker. Falling back to plain <script> tag.',
);
loadMainDartJs();
} }
}); }, 4000);
} });
if (!reg.active && (reg.installing || reg.waiting)) {
// No active web worker and we have installed or are installing
// one for the first time. Simply wait for it to activate.
waitForActivation(reg.installing || reg.waiting);
} else if (!reg.active.scriptURL.endsWith(serviceWorkerVersion)) {
// When the app updates the serviceWorkerVersion changes, so we
// need to ask the service worker to update.
console.log('New service worker available.');
reg.update();
waitForActivation(reg.installing);
} else {
// Existing service worker is still good.
console.log('Loading app from service worker.');
loadMainDartJs();
}
});
// If service worker doesn't succeed in a reasonable amount of time,
// fallback to plaint <script> tag.
setTimeout(() => {
if (!scriptLoaded) {
console.warn(
'Failed to load app from service worker. Falling back to plain <script> tag.',
);
loadMainDartJs();
}
}, 4000);
});
} else { } else {
// Service workers not supported. Just drop the <script> tag. // Service workers not supported. Just drop the <script> tag.
loadMainDartJs(); loadMainDartJs();
} }
</script> </script>
<script src="libs/firebase-app.js?8.10.1"></script> <script src="libs/firebase-app.js?8.10.1"></script>
<script src="libs/firebase-analytics.js?8.10.1"></script> <script src="libs/firebase-analytics.js?8.10.1"></script>
<script> <script>
// Your web app's Firebase configuration // Your web app's Firebase configuration
// For Firebase JS SDK v7.20.0 and later, measurementId is optional // For Firebase JS SDK v7.20.0 and later, measurementId is optional
const firebaseConfig = { const firebaseConfig = {
apiKey: "AIzaSyCgehIZk1aFP0E7wZtYRRqrfvNiNAF39-A", apiKey: "AIzaSyCgehIZk1aFP0E7wZtYRRqrfvNiNAF39-A",
authDomain: "rustdesk.firebaseapp.com", authDomain: "rustdesk.firebaseapp.com",
databaseURL: "https://rustdesk.firebaseio.com", databaseURL: "https://rustdesk.firebaseio.com",
projectId: "rustdesk", projectId: "rustdesk",
storageBucket: "rustdesk.appspot.com", storageBucket: "rustdesk.appspot.com",
messagingSenderId: "768133699366", messagingSenderId: "768133699366",
appId: "1:768133699366:web:d50faf0792cb208d7993e7", appId: "1:768133699366:web:d50faf0792cb208d7993e7",
measurementId: "G-9PEH85N6ZQ" measurementId: "G-9PEH85N6ZQ"
}; };
// Initialize Firebase // Initialize Firebase
firebase.initializeApp(firebaseConfig); firebase.initializeApp(firebaseConfig);
firebase.analytics(); firebase.analytics();
</script> </script>
</body> </body>
</html> </html>

View File

@@ -7,9 +7,12 @@
<script src="ogvjs-1.8.6/ogv.js"></script> <script src="ogvjs-1.8.6/ogv.js"></script>
<script src="./yuv-canvas-1.2.6.js"></script> <script src="./yuv-canvas-1.2.6.js"></script>
<title>Vite App</title> <title>Vite App</title>
<script type="module" crossorigin src="/index.js"></script>
<link rel="modulepreload" href="/vendor.js">
<link rel="stylesheet" href="/index.css">
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>
</body> </body>
</html> </html>

File diff suppressed because one or more lines are too long

797
resources/web/js/package-lock.json generated vendored
View File

@@ -1,803 +1,8 @@
{ {
"name": "web_hbb", "name": "web_hbb",
"version": "1.0.0", "version": "1.0.0",
"lockfileVersion": 2, "lockfileVersion": 1,
"requires": true, "requires": true,
"packages": {
"": {
"name": "web_hbb",
"version": "1.0.0",
"dependencies": {
"fast-sha256": "^1.3.0",
"libsodium": "^0.7.9",
"libsodium-wrappers": "^0.7.9",
"pcm-player": "^0.0.11",
"ts-proto": "^1.141.1",
"wasm-feature-detect": "^1.2.11",
"zstddec": "^0.0.2"
},
"devDependencies": {
"typescript": "4.4.4",
"vite": "2.8"
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.14.54",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.14.54.tgz",
"integrity": "sha512-bZBrLAIX1kpWelV0XemxBZllyRmM6vgFQQG2GdNb+r3Fkp0FOh1NJSvekXDs7jq70k4euu1cryLMfU+mTXlEpw==",
"cpu": [
"loong64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@protobufjs/aspromise": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
"integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="
},
"node_modules/@protobufjs/base64": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz",
"integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="
},
"node_modules/@protobufjs/codegen": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz",
"integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg=="
},
"node_modules/@protobufjs/eventemitter": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz",
"integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q=="
},
"node_modules/@protobufjs/fetch": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz",
"integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==",
"dependencies": {
"@protobufjs/aspromise": "^1.1.1",
"@protobufjs/inquire": "^1.1.0"
}
},
"node_modules/@protobufjs/float": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz",
"integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ=="
},
"node_modules/@protobufjs/inquire": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz",
"integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q=="
},
"node_modules/@protobufjs/path": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz",
"integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA=="
},
"node_modules/@protobufjs/pool": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz",
"integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw=="
},
"node_modules/@protobufjs/utf8": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
"integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="
},
"node_modules/@types/long": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz",
"integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA=="
},
"node_modules/@types/node": {
"version": "18.7.6",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.7.6.tgz",
"integrity": "sha512-EdxgKRXgYsNITy5mjjXjVE/CS8YENSdhiagGrLqjG0pvA2owgJ6i4l7wy/PFZGC0B1/H20lWKN7ONVDNYDZm7A=="
},
"node_modules/@types/object-hash": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/@types/object-hash/-/object-hash-1.3.4.tgz",
"integrity": "sha512-xFdpkAkikBgqBdG9vIlsqffDV8GpvnPEzs0IUtr1v3BEB97ijsFQ4RXVbUZwjFThhB4MDSTUfvmxUD5PGx0wXA=="
},
"node_modules/case-anything": {
"version": "2.1.10",
"resolved": "https://registry.npmjs.org/case-anything/-/case-anything-2.1.10.tgz",
"integrity": "sha512-JczJwVrCP0jPKh05McyVsuOg6AYosrB9XWZKbQzXeDAm2ClE/PJE/BcrrQrVyGYH7Jg8V/LDupmyL4kFlVsVFQ==",
"engines": {
"node": ">=12.13"
},
"funding": {
"url": "https://github.com/sponsors/mesqueeb"
}
},
"node_modules/dataloader": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/dataloader/-/dataloader-1.4.0.tgz",
"integrity": "sha512-68s5jYdlvasItOJnCuI2Q9s4q98g0pCyL3HrcKJu8KNugUl8ahgmZYg38ysLTgQjjXX3H8CJLkAvWrclWfcalw=="
},
"node_modules/detect-libc": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
"integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==",
"bin": {
"detect-libc": "bin/detect-libc.js"
},
"engines": {
"node": ">=0.10"
}
},
"node_modules/dprint-node": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/dprint-node/-/dprint-node-1.0.7.tgz",
"integrity": "sha512-NTZOW9A7ipb0n7z7nC3wftvsbceircwVHSgzobJsEQa+7RnOMbhrfX5IflA6CtC4GA63DSAiHYXa4JKEy9F7cA==",
"dependencies": {
"detect-libc": "^1.0.3"
}
},
"node_modules/esbuild": {
"version": "0.14.54",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.14.54.tgz",
"integrity": "sha512-Cy9llcy8DvET5uznocPyqL3BFRrFXSVqbgpMJ9Wz8oVjZlh/zUSNbPRbov0VX7VxN2JH1Oa0uNxZ7eLRb62pJA==",
"dev": true,
"hasInstallScript": true,
"bin": {
"esbuild": "bin/esbuild"
},
"engines": {
"node": ">=12"
},
"optionalDependencies": {
"@esbuild/linux-loong64": "0.14.54",
"esbuild-android-64": "0.14.54",
"esbuild-android-arm64": "0.14.54",
"esbuild-darwin-64": "0.14.54",
"esbuild-darwin-arm64": "0.14.54",
"esbuild-freebsd-64": "0.14.54",
"esbuild-freebsd-arm64": "0.14.54",
"esbuild-linux-32": "0.14.54",
"esbuild-linux-64": "0.14.54",
"esbuild-linux-arm": "0.14.54",
"esbuild-linux-arm64": "0.14.54",
"esbuild-linux-mips64le": "0.14.54",
"esbuild-linux-ppc64le": "0.14.54",
"esbuild-linux-riscv64": "0.14.54",
"esbuild-linux-s390x": "0.14.54",
"esbuild-netbsd-64": "0.14.54",
"esbuild-openbsd-64": "0.14.54",
"esbuild-sunos-64": "0.14.54",
"esbuild-windows-32": "0.14.54",
"esbuild-windows-64": "0.14.54",
"esbuild-windows-arm64": "0.14.54"
}
},
"node_modules/esbuild-android-64": {
"version": "0.14.54",
"resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.14.54.tgz",
"integrity": "sha512-Tz2++Aqqz0rJ7kYBfz+iqyE3QMycD4vk7LBRyWaAVFgFtQ/O8EJOnVmTOiDWYZ/uYzB4kvP+bqejYdVKzE5lAQ==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-android-arm64": {
"version": "0.14.54",
"resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.14.54.tgz",
"integrity": "sha512-F9E+/QDi9sSkLaClO8SOV6etqPd+5DgJje1F9lOWoNncDdOBL2YF59IhsWATSt0TLZbYCf3pNlTHvVV5VfHdvg==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-darwin-64": {
"version": "0.14.54",
"resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.14.54.tgz",
"integrity": "sha512-jtdKWV3nBviOd5v4hOpkVmpxsBy90CGzebpbO9beiqUYVMBtSc0AL9zGftFuBon7PNDcdvNCEuQqw2x0wP9yug==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-darwin-arm64": {
"version": "0.14.54",
"resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.54.tgz",
"integrity": "sha512-OPafJHD2oUPyvJMrsCvDGkRrVCar5aVyHfWGQzY1dWnzErjrDuSETxwA2HSsyg2jORLY8yBfzc1MIpUkXlctmw==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-freebsd-64": {
"version": "0.14.54",
"resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.54.tgz",
"integrity": "sha512-OKwd4gmwHqOTp4mOGZKe/XUlbDJ4Q9TjX0hMPIDBUWWu/kwhBAudJdBoxnjNf9ocIB6GN6CPowYpR/hRCbSYAg==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-freebsd-arm64": {
"version": "0.14.54",
"resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.54.tgz",
"integrity": "sha512-sFwueGr7OvIFiQT6WeG0jRLjkjdqWWSrfbVwZp8iMP+8UHEHRBvlaxL6IuKNDwAozNUmbb8nIMXa7oAOARGs1Q==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-linux-32": {
"version": "0.14.54",
"resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.14.54.tgz",
"integrity": "sha512-1ZuY+JDI//WmklKlBgJnglpUL1owm2OX+8E1syCD6UAxcMM/XoWd76OHSjl/0MR0LisSAXDqgjT3uJqT67O3qw==",
"cpu": [
"ia32"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-linux-64": {
"version": "0.14.54",
"resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.54.tgz",
"integrity": "sha512-EgjAgH5HwTbtNsTqQOXWApBaPVdDn7XcK+/PtJwZLT1UmpLoznPd8c5CxqsH2dQK3j05YsB3L17T8vE7cp4cCg==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-linux-arm": {
"version": "0.14.54",
"resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.14.54.tgz",
"integrity": "sha512-qqz/SjemQhVMTnvcLGoLOdFpCYbz4v4fUo+TfsWG+1aOu70/80RV6bgNpR2JCrppV2moUQkww+6bWxXRL9YMGw==",
"cpu": [
"arm"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-linux-arm64": {
"version": "0.14.54",
"resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.54.tgz",
"integrity": "sha512-WL71L+0Rwv+Gv/HTmxTEmpv0UgmxYa5ftZILVi2QmZBgX3q7+tDeOQNqGtdXSdsL8TQi1vIaVFHUPDe0O0kdig==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-linux-mips64le": {
"version": "0.14.54",
"resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.54.tgz",
"integrity": "sha512-qTHGQB8D1etd0u1+sB6p0ikLKRVuCWhYQhAHRPkO+OF3I/iSlTKNNS0Lh2Oc0g0UFGguaFZZiPJdJey3AGpAlw==",
"cpu": [
"mips64el"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-linux-ppc64le": {
"version": "0.14.54",
"resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.54.tgz",
"integrity": "sha512-j3OMlzHiqwZBDPRCDFKcx595XVfOfOnv68Ax3U4UKZ3MTYQB5Yz3X1mn5GnodEVYzhtZgxEBidLWeIs8FDSfrQ==",
"cpu": [
"ppc64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-linux-riscv64": {
"version": "0.14.54",
"resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.54.tgz",
"integrity": "sha512-y7Vt7Wl9dkOGZjxQZnDAqqn+XOqFD7IMWiewY5SPlNlzMX39ocPQlOaoxvT4FllA5viyV26/QzHtvTjVNOxHZg==",
"cpu": [
"riscv64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-linux-s390x": {
"version": "0.14.54",
"resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.54.tgz",
"integrity": "sha512-zaHpW9dziAsi7lRcyV4r8dhfG1qBidQWUXweUjnw+lliChJqQr+6XD71K41oEIC3Mx1KStovEmlzm+MkGZHnHA==",
"cpu": [
"s390x"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-netbsd-64": {
"version": "0.14.54",
"resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.54.tgz",
"integrity": "sha512-PR01lmIMnfJTgeU9VJTDY9ZerDWVFIUzAtJuDHwwceppW7cQWjBBqP48NdeRtoP04/AtO9a7w3viI+PIDr6d+w==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-openbsd-64": {
"version": "0.14.54",
"resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.54.tgz",
"integrity": "sha512-Qyk7ikT2o7Wu76UsvvDS5q0amJvmRzDyVlL0qf5VLsLchjCa1+IAvd8kTBgUxD7VBUUVgItLkk609ZHUc1oCaw==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-sunos-64": {
"version": "0.14.54",
"resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.14.54.tgz",
"integrity": "sha512-28GZ24KmMSeKi5ueWzMcco6EBHStL3B6ubM7M51RmPwXQGLe0teBGJocmWhgwccA1GeFXqxzILIxXpHbl9Q/Kw==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"sunos"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-windows-32": {
"version": "0.14.54",
"resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.14.54.tgz",
"integrity": "sha512-T+rdZW19ql9MjS7pixmZYVObd9G7kcaZo+sETqNH4RCkuuYSuv9AGHUVnPoP9hhuE1WM1ZimHz1CIBHBboLU7w==",
"cpu": [
"ia32"
],
"dev": true,
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-windows-64": {
"version": "0.14.54",
"resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.14.54.tgz",
"integrity": "sha512-AoHTRBUuYwXtZhjXZbA1pGfTo8cJo3vZIcWGLiUcTNgHpJJMC1rVA44ZereBHMJtotyN71S8Qw0npiCIkW96cQ==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-windows-arm64": {
"version": "0.14.54",
"resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.54.tgz",
"integrity": "sha512-M0kuUvXhot1zOISQGXwWn6YtS+Y/1RT9WrVIOywZnJHo3jCDyewAc79aKNQWFCQm+xNHVTq9h8dZKvygoXQQRg==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
}
},
"node_modules/fast-sha256": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/fast-sha256/-/fast-sha256-1.3.0.tgz",
"integrity": "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ=="
},
"node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"hasInstallScript": true,
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/function-bind": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
"dev": true
},
"node_modules/has": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
"integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
"dev": true,
"dependencies": {
"function-bind": "^1.1.1"
},
"engines": {
"node": ">= 0.4.0"
}
},
"node_modules/is-core-module": {
"version": "2.10.0",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.10.0.tgz",
"integrity": "sha512-Erxj2n/LDAZ7H8WNJXd9tw38GYM3dv8rk8Zcs+jJuxYTW7sozH+SS8NtrSjVL1/vpLvWi1hxy96IzjJ3EHTJJg==",
"dev": true,
"dependencies": {
"has": "^1.0.3"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/libsodium": {
"version": "0.7.10",
"resolved": "https://registry.npmjs.org/libsodium/-/libsodium-0.7.10.tgz",
"integrity": "sha512-eY+z7hDrDKxkAK+QKZVNv92A5KYkxfvIshtBJkmg5TSiCnYqZP3i9OO9whE79Pwgm4jGaoHgkM4ao/b9Cyu4zQ=="
},
"node_modules/libsodium-wrappers": {
"version": "0.7.10",
"resolved": "https://registry.npmjs.org/libsodium-wrappers/-/libsodium-wrappers-0.7.10.tgz",
"integrity": "sha512-pO3F1Q9NPLB/MWIhehim42b/Fwb30JNScCNh8TcQ/kIc+qGLQch8ag8wb0keK3EP5kbGakk1H8Wwo7v+36rNQg==",
"dependencies": {
"libsodium": "^0.7.0"
}
},
"node_modules/long": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz",
"integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA=="
},
"node_modules/nanoid": {
"version": "3.3.4",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz",
"integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==",
"dev": true,
"bin": {
"nanoid": "bin/nanoid.cjs"
},
"engines": {
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"node_modules/object-hash": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-1.3.1.tgz",
"integrity": "sha512-OSuu/pU4ENM9kmREg0BdNrUDIl1heYa4mBZacJc+vVWz4GtAwu7jO8s4AIt2aGRUTqxykpWzI3Oqnsm13tTMDA==",
"engines": {
"node": ">= 0.10.0"
}
},
"node_modules/path-parse": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
"dev": true
},
"node_modules/pcm-player": {
"version": "0.0.11",
"resolved": "https://registry.npmjs.org/pcm-player/-/pcm-player-0.0.11.tgz",
"integrity": "sha512-+FmX62jiqZa7wDCqSRQ1g3DuU6JNgpymgOLCWhmiE/Lj/M+rOUNqgNwVQX509LdA9dtBtVD3EQQUSp9JqU6upw=="
},
"node_modules/picocolors": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
"integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==",
"dev": true
},
"node_modules/postcss": {
"version": "8.4.16",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.16.tgz",
"integrity": "sha512-ipHE1XBvKzm5xI7hiHCZJCSugxvsdq2mPnsq5+UF+VHCjiBvtDrlxJfMBToWaP9D5XlgNmcFGqoHmUn0EYEaRQ==",
"dev": true,
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/postcss"
}
],
"dependencies": {
"nanoid": "^3.3.4",
"picocolors": "^1.0.0",
"source-map-js": "^1.0.2"
},
"engines": {
"node": "^10 || ^12 || >=14"
}
},
"node_modules/protobufjs": {
"version": "6.11.3",
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.3.tgz",
"integrity": "sha512-xL96WDdCZYdU7Slin569tFX712BxsxslWwAfAhCYjQKGTq7dAU91Lomy6nLLhh/dyGhk/YH4TwTSRxTzhuHyZg==",
"hasInstallScript": true,
"dependencies": {
"@protobufjs/aspromise": "^1.1.2",
"@protobufjs/base64": "^1.1.2",
"@protobufjs/codegen": "^2.0.4",
"@protobufjs/eventemitter": "^1.1.0",
"@protobufjs/fetch": "^1.1.0",
"@protobufjs/float": "^1.0.2",
"@protobufjs/inquire": "^1.1.0",
"@protobufjs/path": "^1.1.2",
"@protobufjs/pool": "^1.1.0",
"@protobufjs/utf8": "^1.1.0",
"@types/long": "^4.0.1",
"@types/node": ">=13.7.0",
"long": "^4.0.0"
},
"bin": {
"pbjs": "bin/pbjs",
"pbts": "bin/pbts"
}
},
"node_modules/resolve": {
"version": "1.22.1",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz",
"integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==",
"dev": true,
"dependencies": {
"is-core-module": "^2.9.0",
"path-parse": "^1.0.7",
"supports-preserve-symlinks-flag": "^1.0.0"
},
"bin": {
"resolve": "bin/resolve"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/rollup": {
"version": "2.77.3",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.77.3.tgz",
"integrity": "sha512-/qxNTG7FbmefJWoeeYJFbHehJ2HNWnjkAFRKzWN/45eNBBF/r8lo992CwcJXEzyVxs5FmfId+vTSTQDb+bxA+g==",
"dev": true,
"bin": {
"rollup": "dist/bin/rollup"
},
"engines": {
"node": ">=10.0.0"
},
"optionalDependencies": {
"fsevents": "~2.3.2"
}
},
"node_modules/source-map-js": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
"integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/supports-preserve-symlinks-flag": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
"dev": true,
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/ts-poet": {
"version": "6.4.1",
"resolved": "https://registry.npmjs.org/ts-poet/-/ts-poet-6.4.1.tgz",
"integrity": "sha512-AjZEs4h2w4sDfwpHMxQKHrTlNh2wRbM5NRXmLz0RiH+yPGtSQFbe9hBpNocU8vqVNgfh0BIOiXR80xDz3kKxUQ==",
"dependencies": {
"dprint-node": "^1.0.7"
}
},
"node_modules/ts-proto": {
"version": "1.141.1",
"resolved": "https://registry.npmjs.org/ts-proto/-/ts-proto-1.141.1.tgz",
"integrity": "sha512-1b7Ka6R96FvFZldHnYPTFy4rzwOo+OTpIP1mBFW0dDwq4WWtSkIVlZ+SokOQSC1TiccNshOJwQC9soVyWfQ7Zg==",
"dependencies": {
"@types/object-hash": "^1.3.0",
"case-anything": "^2.1.10",
"dataloader": "^1.4.0",
"object-hash": "^1.3.1",
"protobufjs": "^6.11.3",
"ts-poet": "^6.2.0",
"ts-proto-descriptors": "1.7.1"
},
"bin": {
"protoc-gen-ts_proto": "protoc-gen-ts_proto"
}
},
"node_modules/ts-proto-descriptors": {
"version": "1.7.1",
"resolved": "https://registry.npmjs.org/ts-proto-descriptors/-/ts-proto-descriptors-1.7.1.tgz",
"integrity": "sha512-oIKUh3K4Xts4v29USGLfUG+2mEk32MsqpgZAOUyUlkrcIdv34yE+k2oZ2Nzngm6cV/JgFdOxRCqeyvmWHuYAyw==",
"dependencies": {
"long": "^4.0.0",
"protobufjs": "^6.8.8"
}
},
"node_modules/typescript": {
"version": "4.4.4",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.4.tgz",
"integrity": "sha512-DqGhF5IKoBl8WNf8C1gu8q0xZSInh9j1kJJMqT3a94w1JzVaBU4EXOSMrz9yDqMT0xt3selp83fuFMQ0uzv6qA==",
"dev": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=4.2.0"
}
},
"node_modules/vite": {
"version": "2.8.6",
"resolved": "https://registry.npmjs.org/vite/-/vite-2.8.6.tgz",
"integrity": "sha512-e4H0QpludOVKkmOsRyqQ7LTcMUDF3mcgyNU4lmi0B5JUbe0ZxeBBl8VoZ8Y6Rfn9eFKYtdXNPcYK97ZwH+K2ug==",
"dev": true,
"dependencies": {
"esbuild": "^0.14.14",
"postcss": "^8.4.6",
"resolve": "^1.22.0",
"rollup": "^2.59.0"
},
"bin": {
"vite": "bin/vite.js"
},
"engines": {
"node": ">=12.2.0"
},
"optionalDependencies": {
"fsevents": "~2.3.2"
},
"peerDependencies": {
"less": "*",
"sass": "*",
"stylus": "*"
},
"peerDependenciesMeta": {
"less": {
"optional": true
},
"sass": {
"optional": true
},
"stylus": {
"optional": true
}
}
},
"node_modules/wasm-feature-detect": {
"version": "1.2.11",
"resolved": "https://registry.npmjs.org/wasm-feature-detect/-/wasm-feature-detect-1.2.11.tgz",
"integrity": "sha512-HUqwaodrQGaZgz1lZaNioIkog9tkeEJjrM3eq4aUL04whXOVDRc/o2EGb/8kV0QX411iAYWEqq7fMBmJ6dKS6w=="
},
"node_modules/zstddec": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/zstddec/-/zstddec-0.0.2.tgz",
"integrity": "sha512-DCo0oxvcvOTGP/f5FA6tz2Z6wF+FIcEApSTu0zV5sQgn9hoT5lZ9YRAKUraxt9oP7l4e8TnNdi8IZTCX6WCkwA=="
}
},
"dependencies": { "dependencies": {
"@esbuild/linux-loong64": { "@esbuild/linux-loong64": {
"version": "0.14.54", "version": "0.14.54",

View File

@@ -3,7 +3,7 @@
"version": "1.0.0", "version": "1.0.0",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "python ./gen_js_from_hbb.py > src/gen_js_from_hbb.ts && python ./ts_proto.py && tsc && yarn vite build", "build": "vite build",
"preview": "vite preview" "preview": "vite preview"
}, },
"devDependencies": { "devDependencies": {

View File

@@ -14,7 +14,8 @@ const HOSTS = [
"rs-us.rustdesk.com", "rs-us.rustdesk.com",
]; ];
let HOST = localStorage.getItem("rendezvous-server") || HOSTS[0]; let HOST = localStorage.getItem("rendezvous-server") || HOSTS[0];
const SCHEMA = "ws://"; //根据协议设置为ws或wss
const SCHEMA=location.protocol=="https:"?"wss://":"ws://";
type MsgboxCallback = (type: string, title: string, text: string) => void; type MsgboxCallback = (type: string, title: string, text: string) => void;
type DrawCallback = (data: Uint8Array) => void; type DrawCallback = (data: Uint8Array) => void;
@@ -99,7 +100,7 @@ export default class Connection {
ws.sendRendezvous({ punch_hole_request }); ws.sendRendezvous({ punch_hole_request });
const msg = (await ws.next()) as rendezvous.RendezvousMessage; const msg = (await ws.next()) as rendezvous.RendezvousMessage;
ws.close(); ws.close();
console.log(new Date() + ": Got relay response"); console.log(new Date() + ": Got relay response", msg);
const phr = msg.punch_hole_response; const phr = msg.punch_hole_response;
const rr = msg.relay_response; const rr = msg.relay_response;
if (phr) { if (phr) {
@@ -236,8 +237,14 @@ export default class Connection {
async msgLoop() { async msgLoop() {
while (true) { while (true) {
const msg = (await this._ws?.next()) as message.Message; const msg = (await this._ws?.next()) as message.Message;
// console.log("msg", msg);
if (msg?.hash) { if (msg?.hash) {
this._hash = msg?.hash; this._hash = msg?.hash;
const tmp = this.getOption('tmppwd')
if(!this._password && tmp){
this._password = Uint8Array.from(JSON.parse("[" + tmp + "]"));
this.setOption('tmppwd', '')
}
if (!this._password) if (!this._password)
this.msgbox("input-password", "Password Required", ""); this.msgbox("input-password", "Password Required", "");
this.login(); this.login();

View File

@@ -1,41 +1,44 @@
import Connection from "./connection"; import Connection from "./connection";
import _sodium from "libsodium-wrappers"; import _sodium from "libsodium-wrappers";
import { CursorData } from "./message"; import {loadVp9} from "./codec";
import { loadVp9 } from "./codec"; import {checkIfRetry, version} from "./gen_js_from_hbb";
import { checkIfRetry, version } from "./gen_js_from_hbb"; import {initZstd, translate} from "./common";
import { initZstd, translate } from "./common";
import PCMPlayer from "pcm-player"; import PCMPlayer from "pcm-player";
import {getServerConf} from "./ljw";
window.myconsole = (...args) => {
console.log(args);
}
window.curConn = undefined; window.curConn = undefined;
window.isMobile = () => { window.isMobile = () => {
return /(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|ipad|iris|kindle|Android|Silk|lge |maemo|midp|mmp|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino/i.test(navigator.userAgent) return /(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|ipad|iris|kindle|Android|Silk|lge |maemo|midp|mmp|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino/i.test(navigator.userAgent)
|| /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(navigator.userAgent.substr(0, 4)); || /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(navigator.userAgent.substr(0, 4));
} }
export function isDesktop() { export function isDesktop() {
return !isMobile(); return !isMobile();
} }
export function msgbox(type, title, text) { export function msgbox(type, title, text) {
if (!type || (type == 'error' && !text)) return; if (!type || (type == 'error' && !text)) return;
const text2 = text.toLowerCase(); const text2 = text.toLowerCase();
var hasRetry = checkIfRetry(type, title, text) ? 'true' : ''; var hasRetry = checkIfRetry(type, title, text) ? 'true' : '';
onGlobalEvent(JSON.stringify({ name: 'msgbox', type, title, text, hasRetry })); onGlobalEvent(JSON.stringify({name: 'msgbox', type, title, text, hasRetry}));
} }
function jsonfyForDart(payload) { function jsonfyForDart(payload) {
var tmp = {}; var tmp = {};
for (const [key, value] of Object.entries(payload)) { for (const [key, value] of Object.entries(payload)) {
if (!key) continue; if (!key) continue;
tmp[key] = value instanceof Uint8Array ? '[' + value.toString() + ']' : JSON.stringify(value); tmp[key] = value instanceof Uint8Array ? '[' + value.toString() + ']' : JSON.stringify(value);
} }
return tmp; return tmp;
} }
export function pushEvent(name, payload) { export function pushEvent(name, payload) {
payload = jsonfyForDart(payload); payload = jsonfyForDart(payload);
payload.name = name; payload.name = name;
onGlobalEvent(JSON.stringify(payload)); onGlobalEvent(JSON.stringify(payload));
} }
let yuvWorker; let yuvWorker;
@@ -45,339 +48,347 @@ let pixels;
let flipPixels; let flipPixels;
let oldSize; let oldSize;
if (YUVCanvas.WebGLFrameSink.isAvailable()) { if (YUVCanvas.WebGLFrameSink.isAvailable()) {
var canvas = document.createElement('canvas'); var canvas = document.createElement('canvas');
yuvCanvas = YUVCanvas.attach(canvas, { webGL: true }); yuvCanvas = YUVCanvas.attach(canvas, {webGL: true});
gl = canvas.getContext("webgl"); gl = canvas.getContext("webgl");
} else { } else {
yuvWorker = new Worker("./yuv.js"); yuvWorker = new Worker("./yuv.js");
} }
let testSpeed = [0, 0]; let testSpeed = [0, 0];
export function draw(frame) { export function draw(frame) {
if (yuvWorker) { if (yuvWorker) {
// frame's (y/u/v).bytes already detached, can not transferrable any more. // frame's (y/u/v).bytes already detached, can not transferrable any more.
yuvWorker.postMessage(frame); yuvWorker.postMessage(frame);
} else { } else {
var tm0 = new Date().getTime(); var tm0 = new Date().getTime();
yuvCanvas.drawFrame(frame); yuvCanvas.drawFrame(frame);
var width = canvas.width; var width = canvas.width;
var height = canvas.height; var height = canvas.height;
var size = width * height * 4; var size = width * height * 4;
if (size != oldSize) { if (size != oldSize) {
pixels = new Uint8Array(size); pixels = new Uint8Array(size);
flipPixels = new Uint8Array(size); flipPixels = new Uint8Array(size);
oldSize = size; oldSize = size;
}
gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
const row = width * 4;
const end = (height - 1) * row;
for (let i = 0; i < size; i += row) {
flipPixels.set(pixels.subarray(i, i + row), end - i);
}
onRgba(flipPixels);
testSpeed[1] += new Date().getTime() - tm0;
testSpeed[0] += 1;
if (testSpeed[0] > 30) {
console.log('gl: ' + parseInt('' + testSpeed[1] / testSpeed[0]));
testSpeed = [0, 0];
}
} }
gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, pixels); /*
const row = width * 4; var testCanvas = document.getElementById("test-yuv-decoder-canvas");
const end = (height - 1) * row; if (testCanvas && currentFrame) {
for (let i = 0; i < size; i += row) { var ctx = testCanvas.getContext("2d");
flipPixels.set(pixels.subarray(i, i + row), end - i); testCanvas.width = frame.format.displayWidth;
testCanvas.height = frame.format.displayHeight;
var img = ctx.createImageData(testCanvas.width, testCanvas.height);
img.data.set(currentFrame);
ctx.putImageData(img, 0, 0);
} }
onRgba(flipPixels); */
testSpeed[1] += new Date().getTime() - tm0;
testSpeed[0] += 1;
if (testSpeed[0] > 30) {
console.log('gl: ' + parseInt('' + testSpeed[1] / testSpeed[0]));
testSpeed = [0, 0];
}
}
/*
var testCanvas = document.getElementById("test-yuv-decoder-canvas");
if (testCanvas && currentFrame) {
var ctx = testCanvas.getContext("2d");
testCanvas.width = frame.format.displayWidth;
testCanvas.height = frame.format.displayHeight;
var img = ctx.createImageData(testCanvas.width, testCanvas.height);
img.data.set(currentFrame);
ctx.putImageData(img, 0, 0);
}
*/
} }
export function sendOffCanvas(c) { export function sendOffCanvas(c) {
let canvas = c.transferControlToOffscreen(); let canvas = c.transferControlToOffscreen();
yuvWorker.postMessage({ canvas }, [canvas]); yuvWorker.postMessage({canvas}, [canvas]);
} }
export function setConn(conn) { export function setConn(conn) {
window.curConn = conn; window.curConn = conn;
} }
export function getConn() { export function getConn() {
return window.curConn; return window.curConn;
} }
export async function startConn(id) { export async function startConn(id) {
setByName('remote_id', id); setByName('remote_id', id);
await curConn.start(id); await curConn.start(id);
} }
export function close() { export function close() {
getConn()?.close(); getConn()?.close();
setConn(undefined); setConn(undefined);
} }
export function newConn() { export function newConn() {
window.curConn?.close(); window.curConn?.close();
const conn = new Connection(); const conn = new Connection();
setConn(conn); setConn(conn);
return conn; return conn;
} }
let sodium; let sodium;
export async function verify(signed, pk) { export async function verify(signed, pk) {
if (!sodium) { if (!sodium) {
await _sodium.ready; await _sodium.ready;
sodium = _sodium; sodium = _sodium;
} }
if (typeof pk == 'string') { if (typeof pk == 'string') {
pk = decodeBase64(pk); pk = decodeBase64(pk);
} }
return sodium.crypto_sign_open(signed, pk); return sodium.crypto_sign_open(signed, pk);
} }
export function decodeBase64(pk) { export function decodeBase64(pk) {
return sodium.from_base64(pk, sodium.base64_variants.ORIGINAL); return sodium.from_base64(pk, sodium.base64_variants.ORIGINAL);
} }
export function genBoxKeyPair() { export function genBoxKeyPair() {
const pair = sodium.crypto_box_keypair(); const pair = sodium.crypto_box_keypair();
const sk = pair.privateKey; const sk = pair.privateKey;
const pk = pair.publicKey; const pk = pair.publicKey;
return [sk, pk]; return [sk, pk];
} }
export function genSecretKey() { export function genSecretKey() {
return sodium.crypto_secretbox_keygen(); return sodium.crypto_secretbox_keygen();
} }
export function seal(unsigned, theirPk, ourSk) { export function seal(unsigned, theirPk, ourSk) {
const nonce = Uint8Array.from(Array(24).fill(0)); const nonce = Uint8Array.from(Array(24).fill(0));
return sodium.crypto_box_easy(unsigned, nonce, theirPk, ourSk); return sodium.crypto_box_easy(unsigned, nonce, theirPk, ourSk);
} }
function makeOnce(value) { function makeOnce(value) {
var byteArray = Array(24).fill(0); var byteArray = Array(24).fill(0);
for (var index = 0; index < byteArray.length && value > 0; index++) { for (var index = 0; index < byteArray.length && value > 0; index++) {
var byte = value & 0xff; var byte = value & 0xff;
byteArray[index] = byte; byteArray[index] = byte;
value = (value - byte) / 256; value = (value - byte) / 256;
} }
return Uint8Array.from(byteArray); return Uint8Array.from(byteArray);
}; };
export function encrypt(unsigned, nonce, key) { export function encrypt(unsigned, nonce, key) {
return sodium.crypto_secretbox_easy(unsigned, makeOnce(nonce), key); return sodium.crypto_secretbox_easy(unsigned, makeOnce(nonce), key);
} }
export function decrypt(signed, nonce, key) { export function decrypt(signed, nonce, key) {
return sodium.crypto_secretbox_open_easy(signed, makeOnce(nonce), key); return sodium.crypto_secretbox_open_easy(signed, makeOnce(nonce), key);
} }
window.setByName = (name, value) => { window.setByName = (name, value) => {
switch (name) { myconsole('setByName', name, value);
case 'remote_id': switch (name) {
localStorage.setItem('remote-id', value); case 'remote_id':
break; localStorage.setItem('remote-id', value);
case 'connect': break;
newConn(); case 'connect':
startConn(value); newConn();
break; startConn(value);
case 'login': break;
value = JSON.parse(value); case 'login':
curConn.setRemember(value.remember == 'true'); value = JSON.parse(value);
curConn.login(value.password); curConn.setRemember(value.remember == 'true');
break; curConn.login(value.password);
case 'close': break;
close(); case 'close':
break; close();
case 'refresh': break;
curConn.refresh(); case 'refresh':
break; curConn.refresh();
case 'reconnect': break;
curConn.reconnect(); case 'reconnect':
break; curConn.reconnect();
case 'toggle_option': break;
curConn.toggleOption(value); case 'toggle_option':
break; curConn.toggleOption(value);
case 'image_quality': break;
curConn.setImageQuality(value); case 'image_quality':
break; curConn.setImageQuality(value);
case 'lock_screen': break;
curConn.lockScreen(); case 'lock_screen':
break; curConn.lockScreen();
case 'ctrl_alt_del': break;
curConn.ctrlAltDel(); case 'ctrl_alt_del':
break; curConn.ctrlAltDel();
case 'switch_display': break;
curConn.switchDisplay(value); case 'switch_display':
break; curConn.switchDisplay(value);
case 'remove': break;
const peers = getPeers(); case 'remove':
delete peers[value]; const peers = getPeers();
localStorage.setItem('peers', JSON.stringify(peers)); delete peers[value];
break; localStorage.setItem('peers', JSON.stringify(peers));
case 'input_key': break;
value = JSON.parse(value); case 'input_key':
curConn.inputKey(value.name, value.down == 'true', value.press == 'true', value.alt == 'true', value.ctrl == 'true', value.shift == 'true', value.command == 'true'); value = JSON.parse(value);
break; curConn.inputKey(value.name, value.down == 'true', value.press == 'true', value.alt == 'true', value.ctrl == 'true', value.shift == 'true', value.command == 'true');
case 'input_string': break;
curConn.inputString(value); case 'input_string':
break; curConn.inputString(value);
case 'send_mouse': break;
let mask = 0; case 'send_mouse':
value = JSON.parse(value); let mask = 0;
switch (value.type) { value = JSON.parse(value);
case 'down': switch (value.type) {
mask = 1; case 'down':
break; mask = 1;
case 'up': break;
mask = 2; case 'up':
break; mask = 2;
case 'wheel': break;
mask = 3; case 'wheel':
break; mask = 3;
} break;
switch (value.buttons) { }
case 'left': switch (value.buttons) {
mask |= 1 << 3; case 'left':
break; mask |= 1 << 3;
case 'right': break;
mask |= 2 << 3; case 'right':
break; mask |= 2 << 3;
case 'wheel': break;
mask |= 4 << 3; case 'wheel':
} mask |= 4 << 3;
curConn.inputMouse(mask, parseInt(value.x || '0'), parseInt(value.y || '0'), value.alt == 'true', value.ctrl == 'true', value.shift == 'true', value.command == 'true'); }
break; curConn.inputMouse(mask, parseInt(value.x || '0'), parseInt(value.y || '0'), value.alt == 'true', value.ctrl == 'true', value.shift == 'true', value.command == 'true');
case 'option': break;
value = JSON.parse(value); case 'option':
localStorage.setItem(value.name, value.value); value = JSON.parse(value);
break; localStorage.setItem(value.name, value.value);
case 'peer_option': if (value.name === 'access_token' && value.value) {
value = JSON.parse(value); getServerConf(value.value);
curConn.setOption(value.name, value.value); }
break; break;
case 'input_os_password': case 'peer_option':
curConn.inputOsPassword(value); value = JSON.parse(value);
break; curConn.setOption(value.name, value.value);
default: break;
break; case 'input_os_password':
} curConn.inputOsPassword(value);
break;
default:
break;
}
} }
window.getByName = (name, arg) => { window.getByName = (name, arg) => {
let v = _getByName(name, arg); let v = _getByName(name, arg);
if (typeof v == 'string' || v instanceof String) return v; myconsole('getByName', name, arg, v);
if (v == undefined || v == null) return ''; if (typeof v == 'string' || v instanceof String) return v;
return JSON.stringify(v); if (v == undefined || v == null) return '';
return JSON.stringify(v);
} }
function getPeersForDart() { function getPeersForDart() {
const peers = []; const peers = [];
for (const [id, value] of Object.entries(getPeers())) { for (const [id, value] of Object.entries(getPeers())) {
if (!id) continue; if (!id) continue;
const tm = value['tm']; const tm = value['tm'];
const info = value['info']; const info = value['info'];
if (!tm || !info) continue; if (!tm || !info) continue;
peers.push([tm, id, info]); peers.push([tm, id, info]);
} }
return peers.sort().reverse().map(x => x.slice(1)); return peers.sort().reverse().map(x => x.slice(1));
} }
function _getByName(name, arg) { function _getByName(name, arg) {
switch (name) { switch (name) {
case 'peers': case 'peers':
return getPeersForDart(); return getPeersForDart();
case 'remote_id': case 'remote_id':
return localStorage.getItem('remote-id'); return localStorage.getItem('remote-id');
case 'remember': case 'remember':
return curConn.getRemember(); return curConn.getRemember();
case 'toggle_option': case 'toggle_option':
return curConn.getOption(arg) || false; return curConn.getOption(arg) || false;
case 'option': case 'option':
return localStorage.getItem(arg); const v = localStorage.getItem(arg);
case 'image_quality': if (arg === 'access_token' && v) {
return curConn.getImageQuality(); getServerConf(v);
case 'translate': }
arg = JSON.parse(arg); return v;
return translate(arg.locale, arg.text); case 'image_quality':
case 'peer_option': return curConn.getImageQuality();
return curConn.getOption(arg); case 'translate':
case 'test_if_valid_server': arg = JSON.parse(arg);
break; return translate(arg.locale, arg.text);
case 'version': case 'peer_option':
return version; return curConn.getOption(arg);
} case 'test_if_valid_server':
return ''; break;
case 'version':
return version;
}
return '';
} }
let opusWorker = new Worker("./libopus.js"); let opusWorker = new Worker("./libopus.js");
let pcmPlayer; let pcmPlayer;
export function initAudio(channels, sampleRate) { export function initAudio(channels, sampleRate) {
pcmPlayer = newAudioPlayer(channels, sampleRate); pcmPlayer = newAudioPlayer(channels, sampleRate);
opusWorker.postMessage({ channels, sampleRate }); opusWorker.postMessage({channels, sampleRate});
} }
export function playAudio(packet) { export function playAudio(packet) {
opusWorker.postMessage(packet, [packet.buffer]); opusWorker.postMessage(packet, [packet.buffer]);
} }
window.init = async () => { window.init = async () => {
if (yuvWorker) { if (yuvWorker) {
yuvWorker.onmessage = (e) => { yuvWorker.onmessage = (e) => {
onRgba(e.data); onRgba(e.data);
}
} }
} opusWorker.onmessage = (e) => {
opusWorker.onmessage = (e) => { pcmPlayer.feed(e.data);
pcmPlayer.feed(e.data); }
} loadVp9(() => {
loadVp9(() => { }); });
await initZstd(); await initZstd();
console.log('init done'); console.log('init done');
} }
export function getPeers() { export function getPeers() {
try { try {
return JSON.parse(localStorage.getItem('peers')) || {}; return JSON.parse(localStorage.getItem('peers')) || {};
} catch (e) { } catch (e) {
return {}; return {};
} }
} }
function newAudioPlayer(channels, sampleRate) { function newAudioPlayer(channels, sampleRate) {
return new PCMPlayer({ return new PCMPlayer({
channels, channels,
sampleRate, sampleRate,
flushingTime: 2000 flushingTime: 2000
}); });
} }
export function copyToClipboard(text) { export function copyToClipboard(text) {
if (window.clipboardData && window.clipboardData.setData) { if (window.clipboardData && window.clipboardData.setData) {
// Internet Explorer-specific code path to prevent textarea being shown while dialog is visible. // Internet Explorer-specific code path to prevent textarea being shown while dialog is visible.
return window.clipboardData.setData("Text", text); return window.clipboardData.setData("Text", text);
} } else if (document.queryCommandSupported && document.queryCommandSupported("copy")) {
else if (document.queryCommandSupported && document.queryCommandSupported("copy")) { var textarea = document.createElement("textarea");
var textarea = document.createElement("textarea"); textarea.textContent = text;
textarea.textContent = text; textarea.style.position = "fixed"; // Prevent scrolling to bottom of page in Microsoft Edge.
textarea.style.position = "fixed"; // Prevent scrolling to bottom of page in Microsoft Edge. document.body.appendChild(textarea);
document.body.appendChild(textarea); textarea.select();
textarea.select(); try {
try { return document.execCommand("copy"); // Security exception may be thrown by some browsers.
return document.execCommand("copy"); // Security exception may be thrown by some browsers. } catch (ex) {
console.warn("Copy to clipboard failed.", ex);
// return prompt("Copy to clipboard: Ctrl+C, Enter", text);
} finally {
document.body.removeChild(textarea);
}
} }
catch (ex) {
console.warn("Copy to clipboard failed.", ex);
// return prompt("Copy to clipboard: Ctrl+C, Enter", text);
}
finally {
document.body.removeChild(textarea);
}
}
} }

99
resources/web/js/src/ljw.js vendored Normal file
View File

@@ -0,0 +1,99 @@
window._gwen = {}
window._gwen.kv = {}
const apiserver = localStorage.getItem('api-server')
function stringToUint8Array(str) {
var arr = [];
for (var i = 0, j = str.length; i < j; ++i) {
arr.push(str.charCodeAt(i));
}
var tmpUint8Array = new Uint8Array(arr);
return tmpUint8Array
}
function getQueryVariable() {
const query = window.location.hash.substring(3);
const vars = query.split("&");
for (var i = 0; i < vars.length; i++) {
var pair = vars[i].split("=");
window._gwen.kv[pair[0]] = pair[1]
}
}
getQueryVariable()
const id = window._gwen.kv.id || ''
if (id) {
localStorage.setItem('remote-id', id)
}
const share_token = window._gwen.kv.share_token || ''
if (share_token) {
fetch(apiserver + "/api/shared-peer", {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({share_token})
}).then(res => res.json()).then(res => {
if (res.code === 0) {
localStorage.setItem('custom-rendezvous-server', res.data.id_server)
localStorage.setItem('key', res.data.key)
const peer = res.data.peer
localStorage.setItem('remote-id', peer.info.id)
peer.tmppwd = stringToUint8Array(window.atob(peer.tmppwd)).toString()
const oldPeers = JSON.parse(localStorage.getItem('peers')) || {}
oldPeers[peer.info.id] = peer
localStorage.setItem('peers', JSON.stringify(oldPeers))
}
})
}
let fetching = false
export function getServerConf(token){
console.log('getServerConf', token)
if(fetching){
return
}
fetching = true
fetch(apiserver + "/api/server-config", {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + token
}
}
).then(res => res.json()).then(res => {
fetching = false
if (res.code === 0) {
if (!localStorage.getItem('custom-rendezvous-server') || !localStorage.getItem('key')) {
localStorage.setItem('custom-rendezvous-server', res.data.id_server)
localStorage.setItem('key', res.data.key)
}
if (res.data.peers) {
const oldPeers = JSON.parse(localStorage.getItem('peers')) || {}
let needUpdate = false
Object.keys(res.data.peers).forEach(k => {
if (!oldPeers[k]) {
oldPeers[k] = res.data.peers[k]
needUpdate = true
} else {
oldPeers[k].info = res.data.peers[k].info
}
if (oldPeers[k].info && oldPeers[k].info.hash && !oldPeers[k].password) {
let p1 = window.atob(oldPeers[k].info.hash)
const pwd = stringToUint8Array(p1)
oldPeers[k].password = pwd.toString()
oldPeers[k].remember = true
}
})
localStorage.setItem('peers', JSON.stringify(oldPeers))
if (needUpdate) {
window.location.reload()
}
}
}
}).catch(_ => {
fetching = false
})
}

View File

@@ -1,2 +1,3 @@
import "./ljw";
import "./globals"; import "./globals";
import "./ui"; import "./ui";

View File

@@ -3,7 +3,9 @@ package service
import ( import (
"Gwen/global" "Gwen/global"
"Gwen/model" "Gwen/model"
"github.com/google/uuid"
"gorm.io/gorm" "gorm.io/gorm"
"strings"
) )
type AddressBookService struct { type AddressBookService struct {
@@ -68,6 +70,14 @@ func (s *AddressBookService) UpdateAddressBook(abs []*model.AddressBook, userId
ab.UserId = userId ab.UserId = userId
if !ok { if !ok {
//添加 //添加
if ab.Platform == "" || ab.Username == "" || ab.Hostname == "" {
peer := AllService.PeerService.FindById(ab.Id)
if peer.RowId != 0 {
ab.Platform = AllService.AddressBookService.PlatformFromOs(peer.Os)
ab.Username = peer.Username
ab.Hostname = peer.Hostname
}
}
tx.Create(ab) tx.Create(ab)
} else { } else {
//更新 //更新
@@ -113,3 +123,33 @@ func (t *AddressBookService) Delete(u *model.AddressBook) error {
func (t *AddressBookService) Update(u *model.AddressBook) error { func (t *AddressBookService) Update(u *model.AddressBook) error {
return global.DB.Model(u).Updates(u).Error return global.DB.Model(u).Updates(u).Error
} }
// ShareByWebClient 分享
func (t *AddressBookService) ShareByWebClient(m *model.ShareRecord) error {
m.ShareToken = uuid.New().String()
return global.DB.Create(m).Error
}
// SharedPeer
func (t *AddressBookService) SharedPeer(shareToken string) *model.ShareRecord {
m := &model.ShareRecord{}
global.DB.Where("share_token = ?", shareToken).First(m)
return m
}
// PlatformFromOs
func (t *AddressBookService) PlatformFromOs(os string) string {
if strings.Contains(os, "Android") || strings.Contains(os, "android") {
return "Android"
}
if strings.Contains(os, "Windows") || strings.Contains(os, "windows") {
return "Windows"
}
if strings.Contains(os, "Linux") || strings.Contains(os, "linux") {
return "Linux"
}
if strings.Contains(os, "mac") || strings.Contains(os, "Mac") {
return "Mac OS"
}
return ""
}

52
service/audit.go Normal file
View File

@@ -0,0 +1,52 @@
package service
import (
"Gwen/global"
"Gwen/model"
"gorm.io/gorm"
)
type AuditService struct {
}
func (as *AuditService) AuditConnList(page, pageSize uint, where func(tx *gorm.DB)) (res *model.AuditConnList) {
res = &model.AuditConnList{}
res.Page = int64(page)
res.PageSize = int64(pageSize)
tx := global.DB.Model(&model.AuditConn{})
if where != nil {
where(tx)
}
tx.Count(&res.Total)
tx.Scopes(Paginate(page, pageSize))
tx.Find(&res.AuditConns)
return
}
// Create 创建
func (as *AuditService) CreateAuditConn(u *model.AuditConn) error {
res := global.DB.Create(u).Error
return res
}
func (as *AuditService) DeleteAuditConn(u *model.AuditConn) error {
return global.DB.Delete(u).Error
}
// Update 更新
func (as *AuditService) UpdateAuditConn(u *model.AuditConn) error {
return global.DB.Model(u).Updates(u).Error
}
// InfoByPeerIdAndConnId
func (as *AuditService) InfoByPeerIdAndConnId(peerId string, connId int64) (res *model.AuditConn) {
res = &model.AuditConn{}
global.DB.Where("peer_id = ? and conn_id = ?", peerId, connId).First(res)
return
}
// InfoById
func (as *AuditService) InfoById(id uint) (res *model.AuditConn) {
res = &model.AuditConn{}
global.DB.Where("id = ?", id).First(res)
return
}

View File

@@ -7,12 +7,13 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt"
"golang.org/x/oauth2" "golang.org/x/oauth2"
"golang.org/x/oauth2/github" "golang.org/x/oauth2/github"
"golang.org/x/oauth2/google" "golang.org/x/oauth2/google"
"gorm.io/gorm" "gorm.io/gorm"
"io" "io"
"net/http"
"net/url"
"strconv" "strconv"
"sync" "sync"
"time" "time"
@@ -167,36 +168,60 @@ func (os *OauthService) GetOauthConfig(op string) (error, *oauth2.Config) {
return errors.New("ConfigNotFound"), nil return errors.New("ConfigNotFound"), nil
} }
func getHTTPClientWithProxy() *http.Client {
if global.Config.Proxy.Enable {
if global.Config.Proxy.Host == "" {
global.Logger.Warn("Proxy is enabled but proxy host is empty.")
return http.DefaultClient
}
proxyURL, err := url.Parse(global.Config.Proxy.Host)
if err != nil {
global.Logger.Warn("Invalid proxy URL: ", err)
return http.DefaultClient
}
transport := &http.Transport{
Proxy: http.ProxyURL(proxyURL),
}
return &http.Client{Transport: transport}
}
return http.DefaultClient
}
func (os *OauthService) GithubCallback(code string) (error error, userData *GithubUserdata) { func (os *OauthService) GithubCallback(code string) (error error, userData *GithubUserdata) {
err, oauthConfig := os.GetOauthConfig(model.OauthTypeGithub) err, oauthConfig := os.GetOauthConfig(model.OauthTypeGithub)
if err != nil { if err != nil {
return err, nil return err, nil
} }
token, err := oauthConfig.Exchange(context.Background(), code)
// 使用代理配置创建 HTTP 客户端
httpClient := getHTTPClientWithProxy()
ctx := context.WithValue(context.Background(), oauth2.HTTPClient, httpClient)
token, err := oauthConfig.Exchange(ctx, code)
if err != nil { if err != nil {
global.Logger.Warn(fmt.Printf("oauthConfig.Exchange() failed: %s\n", err)) global.Logger.Warn("oauthConfig.Exchange() failed: ", err)
error = errors.New("GetOauthTokenError") error = errors.New("GetOauthTokenError")
return return
} }
// 创建一个 HTTP 客户端,并将 access_token 添加到 Authorization 头中 // 使用带有代理的 HTTP 客户端获取用户信息
client := oauthConfig.Client(context.Background(), token) client := oauthConfig.Client(ctx, token)
resp, err := client.Get("https://api.github.com/user") resp, err := client.Get("https://api.github.com/user")
if err != nil { if err != nil {
global.Logger.Warn("failed getting user info: %s\n", err) global.Logger.Warn("failed getting user info: ", err)
error = errors.New("GetOauthUserInfoError") error = errors.New("GetOauthUserInfoError")
return return
} }
defer func(Body io.ReadCloser) { defer func(Body io.ReadCloser) {
err := Body.Close() err := Body.Close()
if err != nil { if err != nil {
global.Logger.Warn("failed closing response body: %s\n", err) global.Logger.Warn("failed closing response body: ", err)
} }
}(resp.Body) }(resp.Body)
// 在这里处理 GitHub 用户信息 // 解析用户信息
if err = json.NewDecoder(resp.Body).Decode(&userData); err != nil { if err = json.NewDecoder(resp.Body).Decode(&userData); err != nil {
global.Logger.Warn("failed decoding user info: %s\n", err) global.Logger.Warn("failed decoding user info: ", err)
error = errors.New("DecodeOauthUserInfoError") error = errors.New("DecodeOauthUserInfoError")
return return
} }
@@ -205,29 +230,39 @@ func (os *OauthService) GithubCallback(code string) (error error, userData *Gith
func (os *OauthService) GoogleCallback(code string) (error error, userData *GoogleUserdata) { func (os *OauthService) GoogleCallback(code string) (error error, userData *GoogleUserdata) {
err, oauthConfig := os.GetOauthConfig(model.OauthTypeGoogle) err, oauthConfig := os.GetOauthConfig(model.OauthTypeGoogle)
token, err := oauthConfig.Exchange(context.Background(), code)
if err != nil { if err != nil {
global.Logger.Warn(fmt.Printf("oauthConfig.Exchange() failed: %s\n", err)) return err, nil
}
// 使用代理配置创建 HTTP 客户端
httpClient := getHTTPClientWithProxy()
ctx := context.WithValue(context.Background(), oauth2.HTTPClient, httpClient)
token, err := oauthConfig.Exchange(ctx, code)
if err != nil {
global.Logger.Warn("oauthConfig.Exchange() failed: ", err)
error = errors.New("GetOauthTokenError") error = errors.New("GetOauthTokenError")
return return
} }
// 创建 HTTP 客户端,并将 access_token 添加到 Authorization 头中
client := oauthConfig.Client(context.Background(), token) // 使用带有代理的 HTTP 客户端获取用户信息
client := oauthConfig.Client(ctx, token)
resp, err := client.Get("https://www.googleapis.com/oauth2/v2/userinfo") resp, err := client.Get("https://www.googleapis.com/oauth2/v2/userinfo")
if err != nil { if err != nil {
global.Logger.Warn("failed getting user info: %s\n", err) global.Logger.Warn("failed getting user info: ", err)
error = errors.New("GetOauthUserInfoError") error = errors.New("GetOauthUserInfoError")
return return
} }
defer func(Body io.ReadCloser) { defer func(Body io.ReadCloser) {
err := Body.Close() err := Body.Close()
if err != nil { if err != nil {
global.Logger.Warn("failed closing response body: %s\n", err) global.Logger.Warn("failed closing response body: ", err)
} }
}(resp.Body) }(resp.Body)
// 解析用户信息
if err = json.NewDecoder(resp.Body).Decode(&userData); err != nil { if err = json.NewDecoder(resp.Body).Decode(&userData); err != nil {
global.Logger.Warn("failed decoding user info: %s\n", err) global.Logger.Warn("failed decoding user info: ", err)
error = errors.New("DecodeOauthUserInfoError") error = errors.New("DecodeOauthUserInfoError")
return return
} }

View File

@@ -71,6 +71,11 @@ func (ps *PeerService) Delete(u *model.Peer) error {
return global.DB.Delete(u).Error return global.DB.Delete(u).Error
} }
// BatchDelete
func (ps *PeerService) BatchDelete(ids []uint) error {
return global.DB.Where("row_id in (?)", ids).Delete(&model.Peer{}).Error
}
// Update 更新 // Update 更新
func (ps *PeerService) Update(u *model.Peer) error { func (ps *PeerService) Update(u *model.Peer) error {
return global.DB.Model(u).Updates(u).Error return global.DB.Model(u).Updates(u).Error

View File

@@ -15,6 +15,7 @@ type Service struct {
*GroupService *GroupService
*OauthService *OauthService
*LoginLogService *LoginLogService
*AuditService
} }
func New() *Service { func New() *Service {

View File

@@ -21,6 +21,11 @@ func (us *UserService) InfoById(id uint) *model.User {
global.DB.Where("id = ?", id).First(u) global.DB.Where("id = ?", id).First(u)
return u return u
} }
func (us *UserService) InfoByUsername(un string) *model.User {
u := &model.User{}
global.DB.Where("username = ?", un).First(u)
return u
}
// InfoByOpenid 根据openid取用户信息 // InfoByOpenid 根据openid取用户信息
func (us *UserService) InfoByOpenid(openid string) *model.User { func (us *UserService) InfoByOpenid(openid string) *model.User {
@@ -262,3 +267,10 @@ func (us *UserService) UserThirdInfo(userId uint, op string) *model.UserThird {
global.DB.Where("user_id = ? and third_type = ?", userId, op).First(ut) global.DB.Where("user_id = ? and third_type = ?", userId, op).First(ut)
return ut return ut
} }
// FindLatestUserIdFromLoginLogByUuid 根据uuid查找最后登录的用户id
func (us *UserService) FindLatestUserIdFromLoginLogByUuid(uuid string) uint {
llog := &model.LoginLog{}
global.DB.Where("uuid = ?", uuid).Order("id desc").First(llog)
return llog.UserId
}