Compare commits

...

22 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
42 changed files with 2725 additions and 913 deletions

453
README.md
View File

@@ -30,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`来控制是否启用
#### 登录 #### 登录
@@ -69,26 +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)
@@ -121,6 +151,8 @@
```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"
@@ -141,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
@@ -197,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
@@ -208,74 +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
- 21119:21119 # web client image: rustdesk/rustdesk-server
image: rustdesk/rustdesk-server command: hbbs -r <relay-server-ip[:port]> -k <key> # 填入个人域名或 IP + hbbr 暴露端口
command: hbbs -r <relay-server-ip[:port]> -k 123456789 # 填入个人域名或 IP + hbbr 暴露端口 volumes:
volumes: - ./data:/root # 自定义挂载目录
- /data/rustdesk/hbbs:/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 映射端口 - 21119:21119 # web client
image: rustdesk/rustdesk-server image: rustdesk/rustdesk-server
command: hbbr -k 123456789 command: hbbr -k <key>
#command: hbbr volumes:
volumes: - ./data:/root
- /data/rustdesk/hbbr:/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=<key>
- RUSTDESK_API_RUSTDESK_KEY=123456789 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的镜像
- 如果使用***自定义KEY***,会需要修改启动脚本,覆盖镜像中的`/etc/s6-overlay/s6-rc.d/hbbr/run`和`/etc/s6-overlay/s6-rc.d/hbbr/run`
- 如果使用的是S6的镜像会需要修改启动脚本覆盖镜像中的`/etc/s6-overlay/s6-rc.d/hbbr/run` 1. 创建`hbbr/run`自定义KEY才需要
和`/etc/s6-overlay/s6-rc.d/hbbr/run`
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

@@ -29,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
@@ -70,17 +100,18 @@ desktop software that provides self-hosted solutions.
![pc_ab](docs/en_img/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/en_img/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/en_img/web_admin.png) ![web_admin](docs/en_img/web_admin.png)
@@ -90,7 +121,7 @@ installation are `admin` `admin`, please change the password immediately.***
![web_resetpwd](docs/en_img/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/en_img/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/en_img/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
@@ -126,6 +157,8 @@ installation are `admin` `admin`, please change the password immediately.***
```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,166 +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_LANG=en \ -e RUSTDESK_API_LANG=en \
-e RUSTDESK_API_RUSTDESK_ID_SERVER=192.168.1.66:21116 \ -e RUSTDESK_API_RUSTDESK_ID_SERVER=192.168.1.66:21116 \
-e RUSTDESK_API_RUSTDESK_RELAY_SERVER=192.168.1.66:21117 \ -e RUSTDESK_API_RUSTDESK_RELAY_SERVER=192.168.1.66:21117 \
-e RUSTDESK_API_RUSTDESK_API_SERVER=http://192.168.1.66:21114 \ -e RUSTDESK_API_RUSTDESK_API_SERVER=http://192.168.1.66:21114 \
-e RUSTDESK_API_RUSTDESK_KEY=123456789 \ -e RUSTDESK_API_RUSTDESK_KEY=abc123456 \
lejianwen/rustdesk-api 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
- 21119:21119 # web client #!/command/with-contenv sh
image: rustdesk/rustdesk-server sleep 2
command: hbbs -r <relay-server-ip[:port]> -k 123456789 # 填入个人域名或 IP + hbbr 暴露端口 cd /data
volumes: PARAMS=
- /data/rustdesk/hbbs:/root # 自定义挂载目录 [ "${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
@@ -388,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

@@ -200,7 +200,7 @@ func getTranslatorForLang(lang string) ut.Translator {
} }
} }
func DatabaseAutoUpdate() { func DatabaseAutoUpdate() {
version := 220 version := 233
db := global.DB db := global.DB
@@ -262,6 +262,7 @@ func Migrate(version uint) {
&model.Oauth{}, &model.Oauth{},
&model.LoginLog{}, &model.LoginLog{},
&model.ShareRecord{}, &model.ShareRecord{},
&model.AuditConn{},
) )
if err != nil { if err != nil {
fmt.Println("migrate err :=>", err) fmt.Println("migrate err :=>", err)

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

@@ -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": [
@@ -1371,6 +1567,18 @@ const docTemplateadmin = `{
"description": "时间", "description": "时间",
"name": "time_ago", "name": "time_ago",
"in": "query" "in": "query"
},
{
"type": "string",
"description": "ID",
"name": "id",
"in": "query"
},
{
"type": "string",
"description": "主机名",
"name": "hostname",
"in": "query"
} }
], ],
"responses": { "responses": {
@@ -1475,7 +1683,7 @@ const docTemplateadmin = `{
"tags": [ "tags": [
"ADMIN" "ADMIN"
], ],
"summary": "服务配置", "summary": "RUSTDESK服务配置",
"responses": { "responses": {
"200": { "200": {
"description": "OK", "description": "OK",
@@ -2456,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": [
@@ -2626,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": {

View File

@@ -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": [
@@ -1364,6 +1560,18 @@
"description": "时间", "description": "时间",
"name": "time_ago", "name": "time_ago",
"in": "query" "in": "query"
},
{
"type": "string",
"description": "ID",
"name": "id",
"in": "query"
},
{
"type": "string",
"description": "主机名",
"name": "hostname",
"in": "query"
} }
], ],
"responses": { "responses": {
@@ -1468,7 +1676,7 @@
"tags": [ "tags": [
"ADMIN" "ADMIN"
], ],
"summary": "服务配置", "summary": "RUSTDESK服务配置",
"responses": { "responses": {
"200": { "200": {
"description": "OK", "description": "OK",
@@ -2449,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": [
@@ -2619,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": {

View File

@@ -141,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:
@@ -254,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:
@@ -618,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:
@@ -651,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:
@@ -1283,6 +1464,14 @@ paths:
in: query in: query
name: time_ago name: time_ago
type: integer 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:
@@ -1355,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.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 50 KiB

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

@@ -77,6 +77,8 @@ func (ct *Peer) Create(c *gin.Context) {
// @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 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]
@@ -96,6 +98,12 @@ func (ct *Peer) List(c *gin.Context) {
lt := time.Now().Unix() + int64(query.TimeAgo) lt := time.Now().Unix() + int64(query.TimeAgo)
tx.Where("last_online_time > ?", lt) 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)
} }

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

@@ -3,6 +3,7 @@ package api
import ( import (
requstform "Gwen/http/request/api" requstform "Gwen/http/request/api"
"Gwen/http/response" "Gwen/http/response"
"Gwen/model"
"Gwen/service" "Gwen/service"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"net/http" "net/http"
@@ -53,7 +54,11 @@ func (i *Index) Heartbeat(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{}) c.JSON(http.StatusOK, gin.H{})
return return
} }
peer.LastOnlineTime = time.Now().Unix() //如果在一分钟以内则不更新
service.AllService.PeerService.Update(peer) 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

@@ -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

@@ -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

@@ -35,5 +35,7 @@ func (f *PeerForm) ToPeer() *model.Peer {
type PeerQuery struct { type PeerQuery struct {
PageQuery PageQuery
TimeAgo int `json:"time_ago" form:"time_ago"` 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

@@ -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"))
@@ -142,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

@@ -47,17 +47,13 @@ func ApiInit(g *gin.Engine) {
frg.POST("/sysinfo", pe.SysInfo) frg.POST("/sysinfo", pe.SysInfo)
} }
{ if global.Config.App.WebClient == 1 {
w := &api.WebClient{} WebClientRoutes(frg)
frg.POST("/shared-peer", w.SharedPeer)
} }
au := &api.Audit{}
//[method:POST] [uri:/api/audit/conn]
frg.POST("/audit/conn", au.AuditConn)
frg.Use(middleware.RustAuth()) frg.Use(middleware.RustAuth())
{
w := &api.WebClient{}
frg.POST("/server-config", w.ServerConfig)
}
{ {
u := &api.User{} u := &api.User{}
frg.GET("/user/info", u.Info) frg.GET("/user/info", u.Info)
@@ -115,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

@@ -16,6 +16,7 @@ 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( Logger: logger.New(
global.Logger, // io writer global.Logger, // io writer
logger.Config{ logger.Config{

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
}

File diff suppressed because one or more lines are too long

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);
}
}
} }

View File

@@ -49,52 +49,51 @@ if (share_token) {
}) })
} }
let fetching = false
const autoWriteServer = () => { export function getServerConf(token){
return setTimeout(() => { console.log('getServerConf', token)
const token = localStorage.getItem('access_token') if(fetching){
if (token && apiserver) { return
fetch(apiserver + "/api/server-config", { }
method: 'POST', fetching = true
headers: { fetch(apiserver + "/api/server-config", {
'Content-Type': 'application/json', method: 'POST',
'Authorization': 'Bearer ' + token 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) {
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()
}
}
}
})
} else {
autoWriteServer()
} }
}, 1000) ).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
})
} }
autoWriteServer()

View File

@@ -5,6 +5,7 @@ import (
"Gwen/model" "Gwen/model"
"github.com/google/uuid" "github.com/google/uuid"
"gorm.io/gorm" "gorm.io/gorm"
"strings"
) )
type AddressBookService struct { type AddressBookService struct {
@@ -69,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 {
//更新 //更新
@@ -127,3 +136,20 @@ func (t *AddressBookService) SharedPeer(shareToken string) *model.ShareRecord {
global.DB.Where("share_token = ?", shareToken).First(m) global.DB.Where("share_token = ?", shareToken).First(m)
return 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

@@ -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
}