Compare commits

...

10 Commits

Author SHA1 Message Date
lejianwen
8cac15f7dd style: Log time 2025-02-07 17:57:28 +08:00
lejianwen
5011e2b7c1 feat: Web sso env (#125) 2025-02-07 17:52:40 +08:00
lejianwen
b0008143b1 docs: Up readme 2025-02-07 17:52:40 +08:00
lejianwen
a3c3ab5a72 style: Up conf 2025-02-07 17:52:40 +08:00
lejianwen
3a16269215 docs: Up readme 2025-02-07 17:52:40 +08:00
lejianwen
151145b0c3 feat: Random Initial Password for Admin (#117) 2025-02-07 17:52:39 +08:00
lejianwen
9c794e9d4b fix(build): Fix no admin in deb (#119 #120) 2025-02-03 13:29:35 +08:00
lejianwen
01f697d279 fix(api): Add Default Token Expire (#113) 2025-02-03 13:18:08 +08:00
lejianwen
6cdc37333b style: webclient 2025-02-03 00:01:10 +08:00
Tao Chen
ae32915565 feat(ldap): Add LDAP
* rename: Admin to AdminGroup

* update

* cleanup

* tmp save group mapping

* add enableControl(not-test)

* verify username exist before create(for LDAP)

* add getAllGroupsDn()

* rename

* adminGroup

* enable TLS Verify

* init for ldap

---------

Co-authored-by: Tao Chen <iamtaochen@outlook.com>
2025-02-02 23:59:52 +08:00
15 changed files with 719 additions and 146 deletions

View File

@@ -203,6 +203,7 @@ jobs:
- name: Build package for ${{ matrix.job.platform }} arch - name: Build package for ${{ matrix.job.platform }} arch
run: | run: |
mv ${{ matrix.job.platform }}/release/apimain debian-build/${{ matrix.job.platform }}/bin/rustdesk-api mv ${{ matrix.job.platform }}/release/apimain debian-build/${{ matrix.job.platform }}/bin/rustdesk-api
mv ${{ matrix.job.platform }}/release/resources/admin resources
chmod -v a+x debian-build/${{ matrix.job.platform }}/bin/* chmod -v a+x debian-build/${{ matrix.job.platform }}/bin/*
mkdir -p data mkdir -p data
cp -vr debian systemd conf data resources runtime debian-build/${{ matrix.job.platform }}/ cp -vr debian systemd conf data resources runtime debian-build/${{ matrix.job.platform }}/

119
README.md
View File

@@ -9,6 +9,7 @@
<img src="https://img.shields.io/badge/gin-v1.9.0-lightBlue"/> <img src="https://img.shields.io/badge/gin-v1.9.0-lightBlue"/>
<img src="https://img.shields.io/badge/gorm-v1.25.7-green"/> <img src="https://img.shields.io/badge/gorm-v1.25.7-green"/>
<img src="https://img.shields.io/badge/swag-v1.16.3-yellow"/> <img src="https://img.shields.io/badge/swag-v1.16.3-yellow"/>
<img src="https://img.shields.io/badge/i18n-7-green"/>
<img src="https://github.com/lejianwen/rustdesk-api/actions/workflows/build.yml/badge.svg"/> <img src="https://github.com/lejianwen/rustdesk-api/actions/workflows/build.yml/badge.svg"/>
</div> </div>
@@ -46,6 +47,7 @@
## 功能 ## 功能
### API 服务 ### API 服务
基本实现了PC端基础的接口。支持Personal版本接口可以通过配置文件`rustdesk.personal`或环境变量`RUSTDESK_API_RUSTDESK_PERSONAL`来控制是否启用 基本实现了PC端基础的接口。支持Personal版本接口可以通过配置文件`rustdesk.personal`或环境变量`RUSTDESK_API_RUSTDESK_PERSONAL`来控制是否启用
@@ -70,23 +72,20 @@
* 使用前后端分离,提供用户友好的管理界面,主要用来管理和展示。前端代码在[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`,密码将在控制台打印,可以通过[命令行](#CLI)更改密码
![img.png](./docs/init_admin_pwd.png)
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)
3. 每个用户可以多个地址簿,也可以将地址簿共享给其他用户 3. 每个用户可以多个地址簿,也可以将地址簿共享给其他用户
4. 分组可以自定义,方便管理,暂时支持两种类型: `共享组``普通组` 4. 分组可以自定义,方便管理,暂时支持两种类型: `共享组``普通组`
5. 可以直接打开webclient方便使用也可以分享给游客游客可以直接通过webclient远程到设备 5. 可以直接打开webclient方便使用也可以分享给游客游客可以直接通过webclient远程到设备
![web_webclient](docs/admin_webclient.png)
6. Oauth,支持了`Github`, `Google` 以及 `OIDC`, 需要创建一个`OAuth App`,然后配置到后台 6. Oauth,支持了`Github`, `Google` 以及 `OIDC`, 需要创建一个`OAuth App`,然后配置到后台
![web_admin_oauth](docs/web_admin_oauth.png)
- 对于`Google``Github`, `Issuer``Scopes`不需要填写. - 对于`Google``Github`, `Issuer``Scopes`不需要填写.
- 对于`OIDC`, `Issuer`是必须的。`Scopes`是可选的,默认为 `openid,profile,email`. 确保可以获取 `sub`,`email``preferred_username` - 对于`OIDC`, `Issuer`是必须的。`Scopes`是可选的,默认为 `openid,profile,email`. 确保可以获取 `sub`,`email``preferred_username`
- `github oauth app``Settings`->`Developer settings`->`OAuth Apps`->`New OAuth App` - `github oauth app``Settings`->`Developer settings`->`OAuth Apps`->`New OAuth App`
@@ -127,6 +126,7 @@
![api_swag](docs/api_swag.png) ![api_swag](docs/api_swag.png)
### CLI ### CLI
```bash ```bash
# 查看帮助 # 查看帮助
./apimain -h ./apimain -h
@@ -181,48 +181,70 @@ proxy:
jwt: jwt:
key: "" key: ""
expire-duration: 360000 expire-duration: 360000
ldap:
enable: false
url: "ldap://ldap.example.com:389"
tls: false
tls-verify: false
base-dn: "dc=example,dc=com"
bind-dn: "cn=admin,dc=example,dc=com"
bind-password: "password"
user:
base-dn: "ou=users,dc=example,dc=com"
enable-attr: "" #The attribute name of the user for enabling, in AD it is "userAccountControl", empty means no enable attribute, all users are enabled
enable-attr-value: "" # The value of the enable attribute when the user is enabled. If you are using AD, just set random value, it will be ignored.
filter: "(cn=*)"
username: "uid" # The attribute name of the user for usernamem if you are using AD, it should be "sAMAccountName"
email: "mail"
first-name: "givenName"
last-name: "sn"
sync: false # If true, the user will be synchronized to the database when the user logs in. If false, the user will be synchronized to the database when the user be created.
admin-group: "cn=admin,dc=example,dc=com" # The group name of the admin group, if the user is in this group, the user will be an admin.
``` ```
### 环境变量 ### 环境变量
变量名前缀是`RUSTDESK_API`,环境变量如果存在将覆盖配置文件中的配置 环境变量和配置文件`conf/config.yaml`中的配置一一对应,变量名前缀是`RUSTDESK_API`
下面表格并未全部列出,可以参考`conf/config.yaml`中的配置。
| 变量名 | 说明 | 示例 | | 变量名 | 说明 | 示例 |
|---------------------------------------------------|---------------------------------------------------------|------------------------------| |---------------------------------------------------|--------------------------------------------------------------------------------|------------------------------|
| TZ | 时区 | Asia/Shanghai | | TZ | 时区 | Asia/Shanghai |
| RUSTDESK_API_LANG | 语言 | `en`,`zh-CN` | | RUSTDESK_API_LANG | 语言 | `en`,`zh-CN` |
| RUSTDESK_API_APP_WEB_CLIENT | 是否启用web-client; 1:启用,0:不启用; 默认启用 | 1 | | RUSTDESK_API_APP_WEB_CLIENT | 是否启用web-client; 1:启用,0:不启用; 默认启用 | 1 |
| RUSTDESK_API_APP_REGISTER | 是否开启注册; `true`, `false` 默认`false` | `false` | | RUSTDESK_API_APP_REGISTER | 是否开启注册; `true`, `false` 默认`false` | `false` |
| RUSTDESK_API_APP_SHOW_SWAGGER | 是否可见swagger文档;`1`显示,`0`不显示,默认`0`不显示 | `1` | | RUSTDESK_API_APP_SHOW_SWAGGER | 是否可见swagger文档;`1`显示,`0`不显示,默认`0`不显示 | `1` |
| RUSTDESK_API_APP_TOKEN_EXPIRE | token有效时长 | `3600` | | RUSTDESK_API_APP_TOKEN_EXPIRE | token有效时长 | `3600` |
| -----ADMIN配置----- | ---------- | ---------- | | -----ADMIN配置----- | ---------- | ---------- |
| RUSTDESK_API_ADMIN_TITLE | 后台标题 | `RustDesk Api Admin` | | RUSTDESK_API_ADMIN_TITLE | 后台标题 | `RustDesk Api Admin` |
| RUSTDESK_API_ADMIN_HELLO | 后台欢迎语,可以使用`html` | | | RUSTDESK_API_ADMIN_HELLO | 后台欢迎语,可以使用`html` | |
| RUSTDESK_API_ADMIN_HELLO_FILE | 后台欢迎语文件,如果内容多,使用文件更方便。<br>会覆盖`RUSTDESK_API_ADMIN_HELLO` | `./conf/admin/hello.html` | | RUSTDESK_API_ADMIN_HELLO_FILE | 后台欢迎语文件,如果内容多,使用文件更方便。<br>会覆盖`RUSTDESK_API_ADMIN_HELLO` | `./conf/admin/hello.html` |
| -----GIN配置----- | ---------- | ---------- | | -----GIN配置----- | ---------- | ---------- |
| RUSTDESK_API_GIN_TRUST_PROXY | 信任的代理IP列表`,`分割,默认信任所有 | 192.168.1.2,192.168.1.3 | | RUSTDESK_API_GIN_TRUST_PROXY | 信任的代理IP列表`,`分割,默认信任所有 | 192.168.1.2,192.168.1.3 |
| -----------GORM配置---------------- | ------------------------------------ | --------------------------- | | -----GORM配置----- | ---------- | --------------------------- |
| RUSTDESK_API_GORM_TYPE | 数据库类型sqlite或者mysql默认sqlite | sqlite | | RUSTDESK_API_GORM_TYPE | 数据库类型sqlite或者mysql默认sqlite | sqlite |
| RUSTDESK_API_GORM_MAX_IDLE_CONNS | 数据库最大空闲连接数 | 10 | | RUSTDESK_API_GORM_MAX_IDLE_CONNS | 数据库最大空闲连接数 | 10 |
| RUSTDESK_API_GORM_MAX_OPEN_CONNS | 数据库最大打开连接数 | 100 | | RUSTDESK_API_GORM_MAX_OPEN_CONNS | 数据库最大打开连接数 | 100 |
| RUSTDESK_API_RUSTDESK_PERSONAL | 是否启用个人版API 1:启用,0:不启用; 默认启用 | 1 | | RUSTDESK_API_RUSTDESK_PERSONAL | 是否启用个人版API 1:启用,0:不启用; 默认启用 | 1 |
| -----MYSQL配置----- | ---------- | ---------- | | -----MYSQL配置----- | ---------- | ---------- |
| RUSTDESK_API_MYSQL_USERNAME | mysql用户名 | root | | RUSTDESK_API_MYSQL_USERNAME | mysql用户名 | root |
| RUSTDESK_API_MYSQL_PASSWORD | mysql密码 | 111111 | | RUSTDESK_API_MYSQL_PASSWORD | mysql密码 | 111111 |
| RUSTDESK_API_MYSQL_ADDR | mysql地址 | 192.168.1.66:3306 | | RUSTDESK_API_MYSQL_ADDR | mysql地址 | 192.168.1.66:3306 |
| RUSTDESK_API_MYSQL_DBNAME | mysql数据库名 | rustdesk | | RUSTDESK_API_MYSQL_DBNAME | mysql数据库名 | rustdesk |
| -----RUSTDESK配置----- | --------------- | ---------- | | -----RUSTDESK配置----- | ---------- | ---------- |
| RUSTDESK_API_RUSTDESK_ID_SERVER | Rustdesk的id服务器地址 | 192.168.1.66:21116 | | RUSTDESK_API_RUSTDESK_ID_SERVER | Rustdesk的id服务器地址 | 192.168.1.66:21116 |
| RUSTDESK_API_RUSTDESK_RELAY_SERVER | Rustdesk的relay服务器地址 | 192.168.1.66:21117 | | RUSTDESK_API_RUSTDESK_RELAY_SERVER | Rustdesk的relay服务器地址 | 192.168.1.66:21117 |
| RUSTDESK_API_RUSTDESK_API_SERVER | Rustdesk的api服务器地址 | http://192.168.1.66:21114 | | RUSTDESK_API_RUSTDESK_API_SERVER | Rustdesk的api服务器地址 | http://192.168.1.66:21114 |
| RUSTDESK_API_RUSTDESK_KEY | Rustdesk的key | 123456789 | | RUSTDESK_API_RUSTDESK_KEY | Rustdesk的key | 123456789 |
| RUSTDESK_API_RUSTDESK_KEY_FILE | Rustdesk存放key的文件 | `./conf/data/id_ed25519.pub` | | RUSTDESK_API_RUSTDESK_KEY_FILE | Rustdesk存放key的文件 | `./conf/data/id_ed25519.pub` |
| RUSTDESK_API_RUSTDESK_WEBCLIENT_MAGIC_QUERYONLINE | Web client v2 中是否启用新的在线状态查询方法; `1`:启用,`0`:不启用,默认不启用 | `0` | | RUSTDESK_API_RUSTDESK_WEBCLIENT_MAGIC_QUERYONLINE | Web client v2 中是否启用新的在线状态查询方法; `1`:启用,`0`:不启用,默认不启用 | `0` |
| ----PROXY配置----- | --------------- | ---------- | | ----PROXY配置----- | ---------- | ---------- |
| RUSTDESK_API_PROXY_ENABLE | 是否启用代理:`false`, `true` | `false` | | RUSTDESK_API_PROXY_ENABLE | 是否启用代理:`false`, `true` | `false` |
| RUSTDESK_API_PROXY_HOST | 代理地址 | `http://127.0.0.1:1080` | | RUSTDESK_API_PROXY_HOST | 代理地址 | `http://127.0.0.1:1080` |
| ----JWT配置---- | -------- | -------- | | ----JWT配置---- | -------- | -------- |
| RUSTDESK_API_JWT_KEY | 自定义JWT KEY,为空则不启用JWT | | | RUSTDESK_API_JWT_KEY | 自定义JWT KEY,为空则不启用JWT<br/>如果没使用`lejianwen/rustdesk-server`中的`MUST_LOGIN`,建议设置为空 | |
| RUSTDESK_API_JWT_EXPIRE_DURATION | JWT有效时间 | 360000 | | RUSTDESK_API_JWT_EXPIRE_DURATION | JWT有效时间 | 360000 |
### 运行 ### 运行
@@ -288,10 +310,11 @@ jwt:
6. 打开浏览器访问`http://<your server[:port]>/_admin/`,默认用户名密码为`admin`,请及时更改密码。 6. 打开浏览器访问`http://<your server[:port]>/_admin/`,默认用户名密码为`admin`,请及时更改密码。
#### 使用我fork后的server-s6镜像运行 #### 使用`lejianwen/server-s6`镜像运行
- 已解决链接超时问题
- 可以强制登录后才能发起链接
- github https://github.com/lejianwen/rustdesk-server - github https://github.com/lejianwen/rustdesk-server
- docker hub https://hub.docker.com/r/lejianwen/rustdesk-server-s6
```yaml ```yaml
networks: networks:
@@ -344,4 +367,4 @@ jwt:
<img src="https://contrib.rocks/image?repo=lejianwen/rustdesk-api" /> <img src="https://contrib.rocks/image?repo=lejianwen/rustdesk-api" />
</a> </a>
## 如果觉得这个项目对你有帮助请给一个star谢谢

View File

@@ -8,6 +8,7 @@ desktop software that provides self-hosted solutions.
<img src="https://img.shields.io/badge/gin-v1.9.0-lightBlue"/> <img src="https://img.shields.io/badge/gin-v1.9.0-lightBlue"/>
<img src="https://img.shields.io/badge/gorm-v1.25.7-green"/> <img src="https://img.shields.io/badge/gorm-v1.25.7-green"/>
<img src="https://img.shields.io/badge/swag-v1.16.3-yellow"/> <img src="https://img.shields.io/badge/swag-v1.16.3-yellow"/>
<img src="https://img.shields.io/badge/i18n-7-green"/>
<img src="https://github.com/lejianwen/rustdesk-api/actions/workflows/build.yml/badge.svg"/> <img src="https://github.com/lejianwen/rustdesk-api/actions/workflows/build.yml/badge.svg"/>
</div> </div>
@@ -69,23 +70,22 @@ Basic implementation of the PC client's primary interfaces.Supports the Personal
* 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/`
installation are `admin` `admin`, please change the password immediately. * For the initial installation, the admin username is `admin`, and the password will be printed in the console. You can change the password via the [command line](#CLI).
![img.png](./docs/init_admin_pwd.png)
1. Admin interface: 1. Admin interface:
![web_admin](docs/en_img/web_admin.png) ![web_admin](docs/en_img/web_admin.png)
2. Regular user interface: 2. Regular user interface:
![web_user](docs/en_img/web_admin_user.png) ![web_user](docs/en_img/web_admin_user.png)
In the top right corner, you can change the password, switch languages, and toggle between `day/night` mode.
![web_resetpwd](docs/en_img/web_resetpwd.png)
3. Each user can have multiple address books, which can also be shared with other users. 3. Each user can have multiple address books, which can also be shared with other users.
4. Groups can be customized for easy management. Currently, two types are supported: `shared group` and `regular group`. 4. Groups can be customized for easy management. Currently, two types are supported: `shared group` and `regular group`.
5. You can directly launch the client or open the web client for convenience; you can also share it with guests, who can remotely access the device via the web client. 5. You can directly launch the client or open the web client for convenience; you can also share it with guests, who can remotely access the device via the web client.
![web_webclient](docs/en_img/admin_webclient.png)
6. OAuth support: Currently, `GitHub`, `Google` and `OIDC` are supported. You need to create an `OAuth App` and configure it in 6. OAuth support: Currently, `GitHub`, `Google` and `OIDC` are supported. You need to create an `OAuth App` and configure it in
the admin panel. the admin panel.
![web_admin_oauth](docs/en_img/web_admin_oauth.png)
- For `Google` and `Github`, you don't need to fill the `Issuer` and `Scpoes` - For `Google` and `Github`, you don't need to fill the `Issuer` and `Scpoes`
- For `OIDC`, you must set the `Issuer`. And `Scopes` is optional which default is `openid,email,profile`, please make sure this `Oauth App` can access `sub`, `email` and `preferred_username` - For `OIDC`, you must set the `Issuer`. And `Scopes` is optional which default is `openid,email,profile`, please make sure this `Oauth App` can access `sub`, `email` and `preferred_username`
- Create a `GitHub OAuth App` - Create a `GitHub OAuth App`
@@ -97,16 +97,15 @@ installation are `admin` `admin`, please change the password immediately.
8. Connection logs 8. Connection logs
9. File transfer logs 9. File transfer logs
10. Server control 10. Server control
- `Simple mode`, some simple commands have been GUI-ized and can be executed directly in the backend
![rustdesk_command_simple](./docs/en_img/rustdesk_command_simple.png)
- `Simple mode`, some simple commands have been GUI-ized and can be executed directly in the backend - `Advanced mode`, commands can be executed directly in the backend
![rustdesk_command_simple](./docs/en_img/rustdesk_command_simple.png)
- `Advanced mode`, commands can be executed directly in the backend
* Official commands can be used * Official commands can be used
* Custom commands can be added * Custom commands can be added
* Custom commands can be executed * Custom commands can be executed
![rustdesk_command_advance](./docs/en_img/rustdesk_command_advance.png) ![rustdesk_command_advance](./docs/en_img/rustdesk_command_advance.png)
@@ -182,48 +181,70 @@ proxy:
jwt: jwt:
key: "" key: ""
expire-duration: 360000 expire-duration: 360000
ldap:
enable: false
url: "ldap://ldap.example.com:389"
tls: false
tls-verify: false
base-dn: "dc=example,dc=com"
bind-dn: "cn=admin,dc=example,dc=com"
bind-password: "password"
user:
base-dn: "ou=users,dc=example,dc=com"
enable-attr: "" #The attribute name of the user for enabling, in AD it is "userAccountControl", empty means no enable attribute, all users are enabled
enable-attr-value: "" # The value of the enable attribute when the user is enabled. If you are using AD, just set random value, it will be ignored.
filter: "(cn=*)"
username: "uid" # The attribute name of the user for usernamem if you are using AD, it should be "sAMAccountName"
email: "mail"
first-name: "givenName"
last-name: "sn"
sync: false # If true, the user will be synchronized to the database when the user logs in. If false, the user will be synchronized to the database when the user be created.
admin-group: "cn=admin,dc=example,dc=com" # The group name of the admin group, if the user is in this group, the user will be an admin.
``` ```
### Environment Variables ### Environment Variables
The prefix for variable names is `RUSTDESK_API`. If environment variables exist, they will override the configurations in the configuration file. The environment variables correspond one-to-one with the configurations in the `conf/config.yaml` file. The prefix for variable names is `RUSTDESK_API`.
The table below does not list all configurations. Please refer to the configurations in `conf/config.yaml`.
| 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` |
| RUSTDESK_API_APP_WEB_CLIENT | web client on/off; 1: on, 0 off, default: 1 | 1 | | RUSTDESK_API_APP_WEB_CLIENT | web client on/off; 1: on, 0 off, default: 1 | 1 |
| RUSTDESK_API_APP_REGISTER | register enable; `true`, `false`; default:`false` | `false` | | RUSTDESK_API_APP_REGISTER | register enable; `true`, `false`; default:`false` | `false` |
| RUSTDESK_API_APP_SHOW_SWAGGER | swagger visible; 1: yes, 0: no; default: 0 | `0` | | RUSTDESK_API_APP_SHOW_SWAGGER | swagger visible; 1: yes, 0: no; default: 0 | `0` |
| RUSTDESK_API_APP_TOKEN_EXPIRE | token expire duration(second) | `3600` | | RUSTDESK_API_APP_TOKEN_EXPIRE | token expire duration(second) | `3600` |
| ----- ADMIN Configuration----- | ---------- | ---------- | | ----- ADMIN Configuration----- | ---------- | ---------- |
| RUSTDESK_API_ADMIN_TITLE | Admin Title | `RustDesk Api Admin` | | RUSTDESK_API_ADMIN_TITLE | Admin Title | `RustDesk Api Admin` |
| RUSTDESK_API_ADMIN_HELLO | Admin welcome message, you can use `html` | | | RUSTDESK_API_ADMIN_HELLO | Admin welcome message, you can use `html` | |
| RUSTDESK_API_ADMIN_HELLO_FILE | Admin welcome message file,<br>will override `RUSTDESK_API_ADMIN_HELLO` | `./conf/admin/hello.html` | | RUSTDESK_API_ADMIN_HELLO_FILE | Admin welcome message file,<br>will override `RUSTDESK_API_ADMIN_HELLO` | `./conf/admin/hello.html` |
| ----- GIN Configuration ----- | --------------------------------------- | ----------------------------- | | ----- GIN Configuration ----- | --------------------------------------- | ----------------------------- |
| RUSTDESK_API_GIN_TRUST_PROXY | Trusted proxy IPs, separated by commas. | 192.168.1.2,192.168.1.3 | | RUSTDESK_API_GIN_TRUST_PROXY | Trusted proxy IPs, separated by commas. | 192.168.1.2,192.168.1.3 |
| ----- GORM Configuration ----- | --------------------------------------- | ----------------------------- | | ----- GORM Configuration ----- | --------------------------------------- | ----------------------------- |
| RUSTDESK_API_GORM_TYPE | Database type (`sqlite` or `mysql`). Default is `sqlite`. | sqlite | | RUSTDESK_API_GORM_TYPE | Database type (`sqlite` or `mysql`). Default is `sqlite`. | sqlite |
| RUSTDESK_API_GORM_MAX_IDLE_CONNS | Maximum idle connections | 10 | | RUSTDESK_API_GORM_MAX_IDLE_CONNS | Maximum idle connections | 10 |
| RUSTDESK_API_GORM_MAX_OPEN_CONNS | Maximum open connections | 100 | | RUSTDESK_API_GORM_MAX_OPEN_CONNS | Maximum open connections | 100 |
| RUSTDESK_API_RUSTDESK_PERSONAL | Open Personal Api 1:Enable,0:Disable | 1 | | RUSTDESK_API_RUSTDESK_PERSONAL | Open Personal Api 1:Enable,0:Disable | 1 |
| ----- MYSQL Configuration ----- | --------------------------------------- | ----------------------------- | | ----- MYSQL Configuration ----- | --------------------------------------- | ----------------------------- |
| RUSTDESK_API_MYSQL_USERNAME | MySQL username | root | | RUSTDESK_API_MYSQL_USERNAME | MySQL username | root |
| RUSTDESK_API_MYSQL_PASSWORD | MySQL password | 111111 | | RUSTDESK_API_MYSQL_PASSWORD | MySQL password | 111111 |
| RUSTDESK_API_MYSQL_ADDR | MySQL address | 192.168.1.66:3306 | | RUSTDESK_API_MYSQL_ADDR | MySQL address | 192.168.1.66:3306 |
| RUSTDESK_API_MYSQL_DBNAME | MySQL database name | rustdesk | | RUSTDESK_API_MYSQL_DBNAME | MySQL database name | rustdesk |
| ----- RUSTDESK Configuration ----- | --------------------------------------- | ----------------------------- | | ----- RUSTDESK Configuration ----- | --------------------------------------- | ----------------------------- |
| RUSTDESK_API_RUSTDESK_ID_SERVER | Rustdesk ID server address | 192.168.1.66:21116 | | RUSTDESK_API_RUSTDESK_ID_SERVER | Rustdesk ID server address | 192.168.1.66:21116 |
| RUSTDESK_API_RUSTDESK_RELAY_SERVER | Rustdesk relay server address | 192.168.1.66:21117 | | RUSTDESK_API_RUSTDESK_RELAY_SERVER | Rustdesk relay server address | 192.168.1.66:21117 |
| RUSTDESK_API_RUSTDESK_API_SERVER | Rustdesk API server address | http://192.168.1.66:21114 | | RUSTDESK_API_RUSTDESK_API_SERVER | Rustdesk API server address | http://192.168.1.66:21114 |
| RUSTDESK_API_RUSTDESK_KEY | Rustdesk key | 123456789 | | RUSTDESK_API_RUSTDESK_KEY | Rustdesk key | 123456789 |
| RUSTDESK_API_RUSTDESK_KEY_FILE | Rustdesk key file | `./conf/data/id_ed25519.pub` | | RUSTDESK_API_RUSTDESK_KEY_FILE | Rustdesk key file | `./conf/data/id_ed25519.pub` |
| RUSTDESK_API_RUSTDESK_WEBCLIENT_MAGIC_QUERYONLINE | New online query method is enabled in the web client v2; '1': Enabled, '0': Disabled, not enabled by default | `0` | | RUSTDESK_API_RUSTDESK_WEBCLIENT_MAGIC_QUERYONLINE | New online query method is enabled in the web client v2; '1': Enabled, '0': Disabled, not enabled by default | `0` |
| ---- PROXY ----- | --------------- | ---------- | | ---- PROXY ----- | --------------- | ---------- |
| RUSTDESK_API_PROXY_ENABLE | proxy_enable :`false`, `true` | `false` | | RUSTDESK_API_PROXY_ENABLE | proxy_enable :`false`, `true` | `false` |
| RUSTDESK_API_PROXY_HOST | proxy_host | `http://127.0.0.1:1080` | | RUSTDESK_API_PROXY_HOST | proxy_host | `http://127.0.0.1:1080` |
| ----JWT---- | -------- | -------- | | ----JWT---- | -------- | -------- |
| RUSTDESK_API_JWT_KEY | JWT KEY. Set empty to disable jwt | | | RUSTDESK_API_JWT_KEY | Custom JWT KEY, if empty JWT is not enabled.<br/>If `MUST_LOGIN` from `lejianwen/rustdesk-server` is not used, it is recommended to leave it empty. | |
| RUSTDESK_API_JWT_EXPIRE_DURATION | JWT expire duration | 360000 | | RUSTDESK_API_JWT_EXPIRE_DURATION | JWT expire duration | 360000 |
### Installation Steps ### Installation Steps
@@ -294,8 +315,9 @@ Download the release from [release](https://github.com/lejianwen/rustdesk-api/re
#### Running with my forked server-s6 image #### Running with my forked server-s6 image
- Connection timeout issue resolved
- Can enforce login before initiating a connection
- github https://github.com/lejianwen/rustdesk-server - github https://github.com/lejianwen/rustdesk-server
- docker hub https://hub.docker.com/r/lejianwen/rustdesk-server-s6
```yaml ```yaml
networks: networks:
@@ -344,3 +366,5 @@ Thanks to everyone who contributed!
<a href="https://github.com/lejianwen/rustdesk-api/graphs/contributors"> <a href="https://github.com/lejianwen/rustdesk-api/graphs/contributors">
<img src="https://contrib.rocks/image?repo=lejianwen/rustdesk-api" /> <img src="https://contrib.rocks/image?repo=lejianwen/rustdesk-api" />
</a> </a>
## If you find this project helpful, please give it a star, thank you!

View File

@@ -12,7 +12,7 @@ import (
"Gwen/lib/upload" "Gwen/lib/upload"
"Gwen/model" "Gwen/model"
"Gwen/service" "Gwen/service"
"fmt" "Gwen/utils"
"github.com/go-redis/redis/v8" "github.com/go-redis/redis/v8"
"github.com/nicksnyder/go-i18n/v2/i18n" "github.com/nicksnyder/go-i18n/v2/i18n"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@@ -54,10 +54,10 @@ var resetPwdCmd = &cobra.Command{
admin := service.AllService.UserService.InfoById(1) admin := service.AllService.UserService.InfoById(1)
err := service.AllService.UserService.UpdatePassword(admin, pwd) err := service.AllService.UserService.UpdatePassword(admin, pwd)
if err != nil { if err != nil {
fmt.Printf("reset password fail! %v \n", err) global.Logger.Error("reset password fail! ", err)
return return
} }
fmt.Printf("reset password success! \n") global.Logger.Info("reset password success! ")
}, },
} }
var resetUserPwdCmd = &cobra.Command{ var resetUserPwdCmd = &cobra.Command{
@@ -70,20 +70,20 @@ var resetUserPwdCmd = &cobra.Command{
pwd := args[1] pwd := args[1]
uid, err := strconv.Atoi(userId) uid, err := strconv.Atoi(userId)
if err != nil { if err != nil {
fmt.Printf("userId must be int! \n") global.Logger.Warn("userId must be int!")
return return
} }
if uid <= 0 { if uid <= 0 {
fmt.Printf("userId must be greater than 0! \n") global.Logger.Warn("userId must be greater than 0! ")
return return
} }
u := service.AllService.UserService.InfoById(uint(uid)) u := service.AllService.UserService.InfoById(uint(uid))
err = service.AllService.UserService.UpdatePassword(u, pwd) err = service.AllService.UserService.UpdatePassword(u, pwd)
if err != nil { if err != nil {
fmt.Printf("reset password fail! %v \n", err) global.Logger.Warn("reset password fail! ", err)
return return
} }
fmt.Printf("reset password success! \n") global.Logger.Info("reset password success!")
}, },
} }
@@ -93,7 +93,7 @@ func init() {
} }
func main() { func main() {
if err := rootCmd.Execute(); err != nil { if err := rootCmd.Execute(); err != nil {
fmt.Println(err) global.Logger.Error(err)
os.Exit(1) os.Exit(1)
} }
} }
@@ -175,7 +175,6 @@ func DatabaseAutoUpdate() {
if global.Config.Gorm.Type == config.TypeMysql { if global.Config.Gorm.Type == config.TypeMysql {
//检查存不存在数据库,不存在则创建 //检查存不存在数据库,不存在则创建
dbName := db.Migrator().CurrentDatabase() dbName := db.Migrator().CurrentDatabase()
fmt.Println("dbName", dbName)
if dbName == "" { if dbName == "" {
dbName = global.Config.Mysql.Dbname dbName = global.Config.Mysql.Dbname
// 移除 DSN 中的数据库名称,以便初始连接时不指定数据库 // 移除 DSN 中的数据库名称,以便初始连接时不指定数据库
@@ -187,18 +186,18 @@ func DatabaseAutoUpdate() {
// 获取底层的 *sql.DB 对象,并确保在程序退出时关闭连接 // 获取底层的 *sql.DB 对象,并确保在程序退出时关闭连接
sqlDBWithoutDB, err := dbWithoutDB.DB() sqlDBWithoutDB, err := dbWithoutDB.DB()
if err != nil { if err != nil {
fmt.Printf("获取底层 *sql.DB 对象失败: %v\n", err) global.Logger.Error("获取底层 *sql.DB 对象失败: %v\n", err)
return return
} }
defer func() { defer func() {
if err := sqlDBWithoutDB.Close(); err != nil { if err := sqlDBWithoutDB.Close(); err != nil {
fmt.Printf("关闭连接失败: %v\n", err) global.Logger.Error("关闭连接失败: %v\n", err)
} }
}() }()
err = dbWithoutDB.Exec("CREATE DATABASE IF NOT EXISTS " + dbName + " DEFAULT CHARSET utf8mb4").Error err = dbWithoutDB.Exec("CREATE DATABASE IF NOT EXISTS " + dbName + " DEFAULT CHARSET utf8mb4").Error
if err != nil { if err != nil {
fmt.Println(err) global.Logger.Error(err)
return return
} }
} }
@@ -235,7 +234,7 @@ func DatabaseAutoUpdate() {
} }
func Migrate(version uint) { func Migrate(version uint) {
fmt.Println("migrating....", version) global.Logger.Info("migrating....", version)
err := global.DB.AutoMigrate( err := global.DB.AutoMigrate(
&model.Version{}, &model.Version{},
&model.User{}, &model.User{},
@@ -255,7 +254,7 @@ func Migrate(version uint) {
&model.ServerCmd{}, &model.ServerCmd{},
) )
if err != nil { if err != nil {
fmt.Println("migrate err :=>", err) global.Logger.Error("migrate err :=>", err)
} }
global.DB.Create(&model.Version{Version: version}) global.DB.Create(&model.Version{Version: version})
//如果是初次则创建一个默认用户 //如果是初次则创建一个默认用户
@@ -289,7 +288,11 @@ func Migrate(version uint) {
IsAdmin: &is_admin, IsAdmin: &is_admin,
GroupId: 1, GroupId: 1,
} }
admin.Password = service.AllService.UserService.EncryptPassword("admin")
// 生成随机密码
pwd := utils.RandomString(8)
global.Logger.Info("Admin Password Is: ", pwd)
admin.Password = service.AllService.UserService.EncryptPassword(pwd)
global.DB.Create(admin) global.DB.Create(admin)
} }

View File

@@ -4,6 +4,7 @@ app:
register: false #是否开启注册 register: false #是否开启注册
show-swagger: 0 # 1:启用 0:禁用 show-swagger: 0 # 1:启用 0:禁用
token-expire: 360000 token-expire: 360000
web-sso: true #web auth sso
admin: admin:
title: "RustDesk Api Admin" title: "RustDesk Api Admin"
hello-file: "./conf/admin/hello.html" #优先使用file hello-file: "./conf/admin/hello.html" #优先使用file
@@ -40,6 +41,27 @@ proxy:
jwt: jwt:
key: "" key: ""
expire-duration: 360000 expire-duration: 360000
ldap:
enable: false
url: "ldap://ldap.example.com:389"
tls: false
tls-verify: false
base-dn: "dc=example,dc=com"
bind-dn: "cn=admin,dc=example,dc=com"
bind-password: "password"
user:
base-dn: "ou=users,dc=example,dc=com"
enable-attr: "" #The attribute name of the user for enabling, in AD it is "userAccountControl", empty means no enable attribute, all users are enabled
enable-attr-value: "" # The value of the enable attribute when the user is enabled. If you are using AD, just set random value, it will be ignored.
filter: "(cn=*)"
username: "uid" # The attribute name of the user for usernamem if you are using AD, it should be "sAMAccountName"
email: "mail"
first-name: "givenName"
last-name: "sn"
sync: false # If true, the user will be synchronized to the database when the user logs in. If false, the user will be synchronized to the database when the user be created.
admin-group: "cn=admin,dc=example,dc=com" # The group name of the admin group, if the user is in this group, the user will be an admin.
redis: redis:
addr: "127.0.0.1:6379" addr: "127.0.0.1:6379"
password: "" password: ""

View File

@@ -2,7 +2,6 @@ package config
import ( import (
"fmt" "fmt"
"github.com/fsnotify/fsnotify"
"github.com/spf13/viper" "github.com/spf13/viper"
"strings" "strings"
) )
@@ -18,6 +17,7 @@ type App struct {
Register bool `mapstructure:"register"` Register bool `mapstructure:"register"`
ShowSwagger int `mapstructure:"show-swagger"` ShowSwagger int `mapstructure:"show-swagger"`
TokenExpire int `mapstructure:"token-expire"` TokenExpire int `mapstructure:"token-expire"`
WebSso bool `mapstructure:"web-sso"`
} }
type Admin struct { type Admin struct {
Title string `mapstructure:"title"` Title string `mapstructure:"title"`
@@ -38,6 +38,7 @@ type Config struct {
Jwt Jwt Jwt Jwt
Rustdesk Rustdesk Rustdesk Rustdesk
Proxy Proxy Proxy Proxy
Ldap Ldap
} }
// Init 初始化配置 // Init 初始化配置
@@ -56,15 +57,19 @@ func Init(rowVal *Config, path string) *viper.Viper {
panic(fmt.Errorf("Fatal error config file: %s \n", err)) panic(fmt.Errorf("Fatal error config file: %s \n", err))
} }
v.WatchConfig() v.WatchConfig()
v.OnConfigChange(func(e fsnotify.Event) {
//配置文件修改监听 /*
fmt.Println("config file changed:", e.Name) //监听配置修改没什么必要
if err2 := v.Unmarshal(rowVal); err2 != nil { v.OnConfigChange(func(e fsnotify.Event) {
fmt.Println(err2) //配置文件修改监听
} fmt.Println("config file changed:", e.Name)
rowVal.Rustdesk.LoadKeyFile() if err2 := v.Unmarshal(rowVal); err2 != nil {
rowVal.Rustdesk.ParsePort() fmt.Println(err2)
}) }
rowVal.Rustdesk.LoadKeyFile()
rowVal.Rustdesk.ParsePort()
})
*/
if err := v.Unmarshal(rowVal); err != nil { if err := v.Unmarshal(rowVal); err != nil {
fmt.Println(err) fmt.Println(err)
} }

36
config/ldap.go Normal file
View File

@@ -0,0 +1,36 @@
package config
type LdapUser struct {
BaseDn string `mapstructure:"base-dn"` // The base DN of the user for searching
EnableAttr string `mapstructure:"enable-attr"` // The attribute name of the user for enabling, in AD it is "userAccountControl", empty means no enable attribute, all users are enabled
EnableAttrValue string `mapstructure:"enable-attr-value"` // The value of the enable attribute when the user is enabled. If you are using AD, just leave it random str, it will be ignored.
Filter string `mapstructure:"filter"`
Username string `mapstructure:"username"`
Email string `mapstructure:"email"`
FirstName string `mapstructure:"first-name"`
LastName string `mapstructure:"last-name"`
Sync bool `mapstructure:"sync"` // Will sync the user's information to the internal database
AdminGroup string `mapstructure:"admin-group"` // Which group is the admin group
}
// type LdapGroup struct {
// BaseDn string `mapstructure:"base-dn"` // The base DN of the group for searching
// Name string `mapstructure:"name"` // The attribute name of the group
// Filter string `mapstructure:"filter"`
// Admin string `mapstructure:"admin"` // Which group is the admin group
// Member string `mapstructure:"member"` // How to get the member of the group: member, uniqueMember, or memberOf (default: member)
// Mode string `mapstructure:"mode"`
// Map map[string]string `mapstructure:"map"` // If mode is "map", map the LDAP group to the internal group
// }
type Ldap struct {
Enable bool `mapstructure:"enable"`
Url string `mapstructure:"url"`
TLS bool `mapstructure:"tls"`
TlsVerify bool `mapstructure:"tls-verify"`
BaseDn string `mapstructure:"base-dn"`
BindDn string `mapstructure:"bind-dn"`
BindPassword string `mapstructure:"bind-password"`
User LdapUser `mapstructure:"user"`
// Group LdapGroup `mapstructure:"group"`
}

BIN
docs/init_admin_pwd.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

13
go.mod
View File

@@ -13,7 +13,7 @@ require (
github.com/go-playground/validator/v10 v10.11.2 github.com/go-playground/validator/v10 v10.11.2
github.com/go-redis/redis/v8 v8.11.4 github.com/go-redis/redis/v8 v8.11.4
github.com/golang-jwt/jwt/v5 v5.2.1 github.com/golang-jwt/jwt/v5 v5.2.1
github.com/google/uuid v1.1.2 github.com/google/uuid v1.6.0
github.com/nicksnyder/go-i18n/v2 v2.4.0 github.com/nicksnyder/go-i18n/v2 v2.4.0
github.com/sirupsen/logrus v1.8.1 github.com/sirupsen/logrus v1.8.1
github.com/spf13/cobra v1.8.1 github.com/spf13/cobra v1.8.1
@@ -22,13 +22,14 @@ require (
github.com/swaggo/gin-swagger v1.6.0 github.com/swaggo/gin-swagger v1.6.0
github.com/swaggo/swag v1.16.3 github.com/swaggo/swag v1.16.3
golang.org/x/oauth2 v0.23.0 golang.org/x/oauth2 v0.23.0
golang.org/x/text v0.18.0 golang.org/x/text v0.21.0
gorm.io/driver/mysql v1.5.7 gorm.io/driver/mysql v1.5.7
gorm.io/driver/sqlite v1.5.6 gorm.io/driver/sqlite v1.5.6
gorm.io/gorm v1.25.7 gorm.io/gorm v1.25.7
) )
require ( require (
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
github.com/KyleBanks/depth v1.2.1 // indirect github.com/KyleBanks/depth v1.2.1 // indirect
github.com/PuerkitoBio/purell v1.1.1 // indirect github.com/PuerkitoBio/purell v1.1.1 // indirect
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
@@ -37,6 +38,8 @@ require (
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/gin-contrib/sse v0.1.0 // indirect github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-asn1-ber/asn1-ber v1.5.7 // indirect
github.com/go-ldap/ldap/v3 v3.4.10 // indirect
github.com/go-openapi/jsonpointer v0.19.5 // indirect github.com/go-openapi/jsonpointer v0.19.5 // indirect
github.com/go-openapi/jsonreference v0.19.6 // indirect github.com/go-openapi/jsonreference v0.19.6 // indirect
github.com/go-openapi/spec v0.20.4 // indirect github.com/go-openapi/spec v0.20.4 // indirect
@@ -70,10 +73,10 @@ require (
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.9 // indirect github.com/ugorji/go/codec v1.2.9 // indirect
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect
golang.org/x/crypto v0.23.0 // indirect golang.org/x/crypto v0.31.0 // indirect
golang.org/x/image v0.13.0 // indirect golang.org/x/image v0.13.0 // indirect
golang.org/x/net v0.25.0 // indirect golang.org/x/net v0.33.0 // indirect
golang.org/x/sys v0.25.0 // indirect golang.org/x/sys v0.28.0 // indirect
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
google.golang.org/protobuf v1.33.0 // indirect google.golang.org/protobuf v1.33.0 // indirect
gopkg.in/ini.v1 v1.63.2 // indirect gopkg.in/ini.v1 v1.63.2 // indirect

View File

@@ -85,7 +85,9 @@ func (l *Login) Login(c *gin.Context) {
// @Router /login-options [get] // @Router /login-options [get]
func (l *Login) LoginOptions(c *gin.Context) { func (l *Login) LoginOptions(c *gin.Context) {
ops := service.AllService.OauthService.GetOauthProviders() ops := service.AllService.OauthService.GetOauthProviders()
ops = append(ops, model.OauthTypeWebauth) if global.Config.App.WebSso {
ops = append(ops, model.OauthTypeWebauth)
}
var oidcItems []map[string]string var oidcItems []map[string]string
for _, v := range ops { for _, v := range ops {
oidcItems = append(oidcItems, map[string]string{"name": v}) oidcItems = append(oidcItems, map[string]string{"name": v})

View File

@@ -21,7 +21,7 @@ type Config struct {
func New(c *Config) *log.Logger { func New(c *Config) *log.Logger {
log.SetFormatter(&nested.Formatter{ log.SetFormatter(&nested.Formatter{
// HideKeys: true, // HideKeys: true,
TimestampFormat: "2006-01-02 15:04:05", TimestampFormat: "[2006-01-02 15:04:05]",
NoColors: true, NoColors: true,
NoFieldsColors: true, NoFieldsColors: true,
//FieldsOrder: []string{"name", "age"}, //FieldsOrder: []string{"name", "age"},

View File

@@ -9649,7 +9649,7 @@ const hn = async (u, e) => {
o = !1 o = !1
} }
return o && i.push(K4(e, "")), i return o && i.push(K4(e, "")), i
}, Ut = 21116, Lt = "rs-ny.rustdesk.com", tt = 100, w4 = "trust-this-device"; }, Ut = 21116, defaultIdServerPort = 21116, Lt = "rs-ny.rustdesk.com", tt = 100, w4 = "trust-this-device";
class Wt { class Wt {
constructor() { constructor() {
@@ -11087,7 +11087,7 @@ function R4(u = !1) {
return I4(e || Lt, u) return I4(e || Lt, u)
} }
function getrUriFromRs(uri, isRelay = false, roffset = 0) { function getUriFromRs(uri, isRelay = false, roffset = 0) {
const p = isHttps() ? "wss://" : "ws://" const p = isHttps() ? "wss://" : "ws://"
const [domain, uriport] = uri.split(":") const [domain, uriport] = uri.split(":")
if (isHttps() && (!uriport)) { if (isHttps() && (!uriport)) {
@@ -11096,7 +11096,7 @@ function getrUriFromRs(uri, isRelay = false, roffset = 0) {
if (uriport) { if (uriport) {
const port = parseInt(uriport); const port = parseInt(uriport);
uri = domain + ":" + (port + (isRelay ? roffset || 3 : 2)) uri = domain + ":" + (port + (isRelay ? roffset || 3 : 2))
} else uri += ":" + (Ut + (isRelay ? 3 : 2)); } else uri += ":" + (defaultIdServerPort + (isRelay ? 3 : 2));
return p + uri return p + uri
} }
@@ -11105,7 +11105,7 @@ function isHttps() {
} }
function I4(u, e = !1, i = 0) { function I4(u, e = !1, i = 0) {
return getrUriFromRs(u, e, i) return getUriFromRs(u, e, i)
} }
function wn() { function wn() {
@@ -11239,7 +11239,7 @@ async function Pn(u) {
o.close(), console.error("Failed to query online states, no online response") o.close(), console.error("Failed to query online states, no online response")
} }
} }
const Rn = "rustdesk-client"; const Rn = "rustdesk-client";

425
service/ldap.go Normal file
View File

@@ -0,0 +1,425 @@
package service
import (
"crypto/tls"
"errors"
"fmt"
"github.com/go-ldap/ldap/v3"
"strconv"
"strings"
"Gwen/config"
"Gwen/global"
"Gwen/model"
)
// LdapService is responsible for LDAP authentication and user synchronization.
type LdapService struct {
}
// LdapUser represents the user attributes retrieved from LDAP.
type LdapUser struct {
Dn string
Username string
Email string
FirstName string
LastName string
MemberOf []string
EnableAttrValue string
Enabled bool
}
// Name returns the full name of an LDAP user.
func (lu *LdapUser) Name() string {
return fmt.Sprintf("%s %s", lu.FirstName, lu.LastName)
}
// ToUser merges the LdapUser data into a provided *model.User.
// If 'u' is nil, it creates and returns a new *model.User.
func (lu *LdapUser) ToUser(u *model.User) *model.User {
if u == nil {
u = &model.User{}
}
u.Username = lu.Username
u.Email = lu.Email
u.Nickname = lu.Name()
return u
}
// connectAndBind creates an LDAP connection, optionally starts TLS, and then binds using the provided credentials.
func (ls *LdapService) connectAndBind(cfg *config.Ldap, username, password string) (*ldap.Conn, error) {
conn, err := ldap.DialURL(cfg.Url)
if err != nil {
return nil, fmt.Errorf("failed to dial LDAP: %w", err)
}
if cfg.TLS {
// WARNING: InsecureSkipVerify: true is not recommended for production
if err = conn.StartTLS(&tls.Config{InsecureSkipVerify: !cfg.TlsVerify}); err != nil {
conn.Close()
return nil, fmt.Errorf("failed to start TLS: %w", err)
}
}
// Bind as the "service" user
if err = conn.Bind(username, password); err != nil {
conn.Close()
return nil, fmt.Errorf("failed to bind with service account: %w", err)
}
return conn, nil
}
// connectAndBindAdmin creates an LDAP connection, optionally starts TLS, and then binds using the admin credentials.
func (ls *LdapService) connectAndBindAdmin(cfg *config.Ldap) (*ldap.Conn, error) {
return ls.connectAndBind(cfg, cfg.BindDn, cfg.BindPassword)
}
// verifyCredentials checks the provided username and password against LDAP.
func (ls *LdapService) verifyCredentials(cfg *config.Ldap, username, password string) error {
ldapConn, err := ls.connectAndBind(cfg, username, password)
if err != nil {
return err
}
defer ldapConn.Close()
return nil
}
// Authenticate checks the provided username and password against LDAP.
// Returns the corresponding *model.User if successful, or an error if not.
func (ls *LdapService) Authenticate(username, password string) (*model.User, error) {
cfg := &global.Config.Ldap
// 1. Use a service bind to search for the user DN
sr, err := ls.usernameSearchResult(cfg, username)
if err != nil {
return nil, fmt.Errorf("LDAP search request failed: %w", err)
}
if len(sr.Entries) != 1 {
return nil, errors.New("user does not exist or too many entries returned")
}
entry := sr.Entries[0]
userDN := entry.DN
err = ls.verifyCredentials(cfg, userDN, password)
if err != nil {
return nil, fmt.Errorf("LDAP authentication failed: %w", err)
}
ldapUser := ls.userResultToLdapUser(cfg, entry)
if !ldapUser.Enabled {
return nil, errors.New("UserDisabledAtLdap")
}
user, err := ls.mapToLocalUser(cfg, ldapUser)
if err != nil {
return nil, fmt.Errorf("failed to map LDAP user to local user: %w", err)
}
return user, nil
}
// mapToLocalUser checks whether the user exists locally; if not, creates one.
// If the user exists and Ldap.Sync is enabled, it updates local info.
func (ls *LdapService) mapToLocalUser(cfg *config.Ldap, lu *LdapUser) (*model.User, error) {
userService := &UserService{}
localUser := userService.InfoByUsername(lu.Username)
isAdmin := ls.isUserAdmin(cfg, lu)
// If the user doesn't exist in local DB, create a new one
if localUser.Id == 0 {
newUser := lu.ToUser(nil)
// Typically, you dont store LDAP user passwords locally.
// If needed, you can set a random password here.
newUser.IsAdmin = &isAdmin
if err := global.DB.Create(newUser).Error; err != nil {
return nil, fmt.Errorf("failed to create new user: %w", err)
}
return userService.InfoByUsername(lu.Username), nil
}
// If the user already exists and sync is enabled, update local info
if cfg.User.Sync {
originalEmail := localUser.Email
originalNickname := localUser.Nickname
originalIsAdmin := localUser.IsAdmin
lu.ToUser(localUser) // merges LDAP data into the existing user
localUser.IsAdmin = &isAdmin
if err := userService.Update(localUser); err != nil {
// If the update fails, revert to original data
localUser.Email = originalEmail
localUser.Nickname = originalNickname
localUser.IsAdmin = originalIsAdmin
}
}
return localUser, nil
}
// IsUsernameExists checks if a username exists in LDAP (can be useful for local registration checks).
func (ls *LdapService) IsUsernameExists(username string) bool {
cfg := &global.Config.Ldap
if !cfg.Enable {
return false
}
sr, err := ls.usernameSearchResult(cfg, username)
if err != nil {
return false
}
return len(sr.Entries) > 0
}
// IsEmailExists checks if an email exists in LDAP (can be useful for local registration checks).
func (ls *LdapService) IsEmailExists(email string) bool {
cfg := &global.Config.Ldap
if !cfg.Enable {
return false
}
sr, err := ls.emailSearchResult(cfg, email)
if err != nil {
return false
}
return len(sr.Entries) > 0
}
// usernameSearchResult returns the search result for the given username.
func (ls *LdapService) usernameSearchResult(cfg *config.Ldap, username string) (*ldap.SearchResult, error) {
// Build the combined filter for the username
filter := ls.filterField(ls.fieldUsername(cfg), username)
// Create the *ldap.SearchRequest
searchRequest := ls.buildUserSearchRequest(cfg, filter)
return ls.searchResult(cfg, searchRequest)
}
// emailSearchResult returns the search result for the given email.
func (ls *LdapService) emailSearchResult(cfg *config.Ldap, email string) (*ldap.SearchResult, error) {
filter := ls.filterField(ls.fieldEmail(cfg), email)
searchRequest := ls.buildUserSearchRequest(cfg, filter)
return ls.searchResult(cfg, searchRequest)
}
func (ls *LdapService) searchResult(cfg *config.Ldap, searchRequest *ldap.SearchRequest) (*ldap.SearchResult, error) {
ldapConn, err := ls.connectAndBindAdmin(cfg)
if err != nil {
return nil, err
}
defer ldapConn.Close()
return ldapConn.Search(searchRequest)
}
// buildUserSearchRequest constructs an LDAP SearchRequest for users given a filter.
func (ls *LdapService) buildUserSearchRequest(cfg *config.Ldap, filter string) *ldap.SearchRequest {
baseDn := ls.baseDnUser(cfg) // user-specific base DN, or fallback
filterConfig := cfg.User.Filter
if filterConfig == "" {
filterConfig = "(cn=*)"
}
// Combine the default filter with our field filter, e.g. (&(cn=*)(uid=jdoe))
combinedFilter := fmt.Sprintf("(&%s%s)", filterConfig, filter)
attributes := ls.buildUserAttributes(cfg)
return ldap.NewSearchRequest(
baseDn,
ldap.ScopeWholeSubtree,
ldap.NeverDerefAliases,
0, // unlimited search results
0, // no server-side time limit
false, // typesOnly
combinedFilter,
attributes,
nil,
)
}
// buildUserAttributes returns the list of attributes we want from LDAP user searches.
func (ls *LdapService) buildUserAttributes(cfg *config.Ldap) []string {
return []string{
"dn",
ls.fieldUsername(cfg),
ls.fieldEmail(cfg),
ls.fieldFirstName(cfg),
ls.fieldLastName(cfg),
ls.fieldMemberOf(),
ls.fieldUserEnableAttr(cfg),
}
}
// userResultToLdapUser maps an *ldap.Entry to our LdapUser struct.
func (ls *LdapService) userResultToLdapUser(cfg *config.Ldap, entry *ldap.Entry) *LdapUser {
lu := &LdapUser{
Dn: entry.DN,
Username: entry.GetAttributeValue(ls.fieldUsername(cfg)),
Email: entry.GetAttributeValue(ls.fieldEmail(cfg)),
FirstName: entry.GetAttributeValue(ls.fieldFirstName(cfg)),
LastName: entry.GetAttributeValue(ls.fieldLastName(cfg)),
MemberOf: entry.GetAttributeValues(ls.fieldMemberOf()),
EnableAttrValue: entry.GetAttributeValue(ls.fieldUserEnableAttr(cfg)),
}
// Check if the user is enabled based on the LDAP configuration
ls.isUserEnabled(cfg, lu)
return lu
}
// filterField helps build simple attribute filters, e.g. (uid=username).
func (ls *LdapService) filterField(field, value string) string {
return fmt.Sprintf("(%s=%s)", field, value)
}
// fieldUsername returns the configured username attribute or "uid" if not set.
func (ls *LdapService) fieldUsername(cfg *config.Ldap) string {
if cfg.User.Username == "" {
return "uid"
}
return cfg.User.Username
}
// fieldEmail returns the configured email attribute or "mail" if not set.
func (ls *LdapService) fieldEmail(cfg *config.Ldap) string {
if cfg.User.Email == "" {
return "mail"
}
return cfg.User.Email
}
// fieldFirstName returns the configured first name attribute or "givenName" if not set.
func (ls *LdapService) fieldFirstName(cfg *config.Ldap) string {
if cfg.User.FirstName == "" {
return "givenName"
}
return cfg.User.FirstName
}
// fieldLastName returns the configured last name attribute or "sn" if not set.
func (ls *LdapService) fieldLastName(cfg *config.Ldap) string {
if cfg.User.LastName == "" {
return "sn"
}
return cfg.User.LastName
}
func (ls *LdapService) fieldMemberOf() string {
return "memberOf"
}
func (ls *LdapService) fieldUserEnableAttr(cfg *config.Ldap) string {
if cfg.User.EnableAttr == "" {
return "userAccountControl"
}
return cfg.User.EnableAttr
}
// baseDnUser returns the user-specific base DN or the global base DN if none is set.
func (ls *LdapService) baseDnUser(cfg *config.Ldap) string {
if cfg.User.BaseDn == "" {
return cfg.BaseDn
}
return cfg.User.BaseDn
}
// isUserAdmin checks if the user is a member of the admin group.
func (ls *LdapService) isUserAdmin(cfg *config.Ldap, ldapUser *LdapUser) bool {
// Check if the admin group is configured
adminGroup := cfg.User.AdminGroup
if adminGroup == "" {
return false
}
// Check "memberOf" directly
if len(ldapUser.MemberOf) > 0 {
for _, group := range ldapUser.MemberOf {
if group == adminGroup {
return true
}
}
return false
}
// For "member" attribute, perform a reverse search on the group
member := "member"
userDN := ldap.EscapeFilter(ldapUser.Dn)
adminGroupDn := ldap.EscapeFilter(adminGroup)
groupFilter := fmt.Sprintf("(%s=%s)", member, userDN)
// Create the LDAP search request
groupSearchRequest := ldap.NewSearchRequest(
adminGroupDn,
ldap.ScopeWholeSubtree,
ldap.NeverDerefAliases,
0, // Unlimited search results
0, // No time limit
false, // Return both attributes and DN
groupFilter,
[]string{"dn"},
nil,
)
// Perform the group search
groupResult, err := ls.searchResult(cfg, groupSearchRequest)
if err != nil {
return false
}
// If any results are returned, the user is part of the admin group
if len(groupResult.Entries) > 0 {
return true
}
return false
}
// isUserEnabled checks if the user is enabled based on the LDAP configuration.
// If no enable attribute or value is set, all users are considered enabled by default.
func (ls *LdapService) isUserEnabled(cfg *config.Ldap, ldapUser *LdapUser) bool {
// Retrieve the enable attribute and expected value from the configuration
enableAttr := cfg.User.EnableAttr
enableAttrValue := cfg.User.EnableAttrValue
// If no enable attribute or value is configured, consider all users as enabled
if enableAttr == "" || enableAttrValue == "" {
ldapUser.Enabled = true
return true
}
// Normalize the enable attribute for comparison
enableAttr = strings.ToLower(enableAttr)
// Handle Active Directory's userAccountControl attribute
if enableAttr == "useraccountcontrol" {
// Parse the userAccountControl value
userAccountControl, err := strconv.Atoi(ldapUser.EnableAttrValue)
if err != nil {
fmt.Printf("[ERROR] Invalid userAccountControl value: %v\n", err)
ldapUser.Enabled = false
return false
}
// Account is disabled if the ACCOUNTDISABLE flag (0x2) is set
const ACCOUNTDISABLE = 0x2
ldapUser.Enabled = (userAccountControl&ACCOUNTDISABLE == 0)
return ldapUser.Enabled
}
// For other attributes, perform a direct comparison with the expected value
ldapUser.Enabled = (ldapUser.EnableAttrValue == enableAttrValue)
return ldapUser.Enabled
}
// getAttrOfDn retrieves the value of an attribute for a given DN.
func (ls *LdapService) getAttrOfDn(cfg *config.Ldap, dn, attr string) string {
searchRequest := ldap.NewSearchRequest(
ldap.EscapeFilter(dn),
ldap.ScopeBaseObject,
ldap.NeverDerefAliases,
0, // unlimited search results
0, // no server-side time limit
false, // typesOnly
"(objectClass=*)",
[]string{attr},
nil,
)
sr, err := ls.searchResult(cfg, searchRequest)
if err != nil {
return ""
}
if len(sr.Entries) == 0 {
return ""
}
return sr.Entries[0].GetAttributeValue(attr)
}

View File

@@ -18,6 +18,7 @@ type Service struct {
*AuditService *AuditService
*ShareRecordService *ShareRecordService
*ServerCmdService *ServerCmdService
*LdapService
} }
func New() *Service { func New() *Service {

View File

@@ -46,6 +46,14 @@ func (us *UserService) InfoByOpenid(openid string) *model.User {
// InfoByUsernamePassword 根据用户名密码取用户信息 // InfoByUsernamePassword 根据用户名密码取用户信息
func (us *UserService) InfoByUsernamePassword(username, password string) *model.User { func (us *UserService) InfoByUsernamePassword(username, password string) *model.User {
if global.Config.Ldap.Enable {
u, err := AllService.LdapService.Authenticate(username, password)
if err == nil {
return u
}
global.Logger.Error("LDAP authentication failed, %v", err)
global.Logger.Warn("Fallback to local database")
}
u := &model.User{} u := &model.User{}
global.DB.Where("username = ? and password = ?", username, us.EncryptPassword(password)).First(u) global.DB.Where("username = ? and password = ?", username, us.EncryptPassword(password)).First(u)
return u return u
@@ -82,7 +90,7 @@ func (us *UserService) Login(u *model.User, llog *model.LoginLog) *model.UserTok
Token: token, Token: token,
DeviceUuid: llog.Uuid, DeviceUuid: llog.Uuid,
DeviceId: llog.DeviceId, DeviceId: llog.DeviceId,
ExpiredAt: time.Now().Add(time.Second * time.Duration(global.Config.App.TokenExpire)).Unix(), ExpiredAt: us.UserTokenExpireTimestamp(),
} }
global.DB.Create(ut) global.DB.Create(ut)
llog.UserTokenId = ut.UserId llog.UserTokenId = ut.UserId
@@ -156,6 +164,9 @@ func (us *UserService) CheckUserEnable(u *model.User) bool {
// Create 创建 // Create 创建
func (us *UserService) Create(u *model.User) error { func (us *UserService) Create(u *model.User) error {
// The initial username should be formatted, and the username should be unique // The initial username should be formatted, and the username should be unique
if us.IsUsernameExists(u.Username) {
return errors.New("UsernameExists")
}
u.Username = us.formatUsername(u.Username) u.Username = us.formatUsername(u.Username)
u.Password = us.EncryptPassword(u.Password) u.Password = us.EncryptPassword(u.Password)
res := global.DB.Create(u).Error res := global.DB.Create(u).Error
@@ -343,13 +354,10 @@ func (us *UserService) RegisterByOauth(oauthUser *model.OauthUser, op string) (e
// GenerateUsernameByOauth 生成用户名 // GenerateUsernameByOauth 生成用户名
func (us *UserService) GenerateUsernameByOauth(name string) string { func (us *UserService) GenerateUsernameByOauth(name string) string {
u := &model.User{} for us.IsUsernameExists(name) {
global.DB.Where("username = ?", name).First(u) name += strconv.Itoa(rand.Intn(10)) // Append a random digit (0-9)
if u.Id == 0 {
return name
} }
name = name + strconv.FormatInt(rand.Int63n(10), 10) return name
return us.GenerateUsernameByOauth(name)
} }
// UserThirdsByUserId // UserThirdsByUserId
@@ -394,15 +402,18 @@ func (us *UserService) IsPasswordEmptyByUser(u *model.User) bool {
return us.IsPasswordEmptyById(u.Id) return us.IsPasswordEmptyById(u.Id)
} }
// Register 注册 // Register 注册, 如果用户名已存在则返回nil
func (us *UserService) Register(username string, email string, password string) *model.User { func (us *UserService) Register(username string, email string, password string) *model.User {
u := &model.User{ u := &model.User{
Username: username, Username: username,
Email: email, Email: email,
Password: us.EncryptPassword(password), Password: password,
GroupId: 1, GroupId: 1,
} }
global.DB.Create(u) err := us.Create(u)
if err != nil {
return nil
}
return u return u
} }
@@ -451,8 +462,17 @@ func (us *UserService) getAdminUserCount() int64 {
return count return count
} }
// UserTokenExpireTimestamp 生成用户token过期时间
func (us *UserService) UserTokenExpireTimestamp() int64 {
exp := global.Config.App.TokenExpire
if exp == 0 {
exp = 3600 * 24 * 7
}
return time.Now().Add(time.Second * time.Duration(exp)).Unix()
}
func (us *UserService) RefreshAccessToken(ut *model.UserToken) { func (us *UserService) RefreshAccessToken(ut *model.UserToken) {
ut.ExpiredAt = time.Now().Add(time.Second * time.Duration(global.Config.App.TokenExpire)).Unix() ut.ExpiredAt = us.UserTokenExpireTimestamp()
global.DB.Model(ut).Update("expired_at", ut.ExpiredAt) global.DB.Model(ut).Update("expired_at", ut.ExpiredAt)
} }
func (us *UserService) AutoRefreshAccessToken(ut *model.UserToken) { func (us *UserService) AutoRefreshAccessToken(ut *model.UserToken) {
@@ -468,3 +488,11 @@ func (us *UserService) BatchDeleteUserToken(ids []uint) error {
func (us *UserService) VerifyJWT(token string) (uint, error) { func (us *UserService) VerifyJWT(token string) (uint, error) {
return global.Jwt.ParseToken(token) return global.Jwt.ParseToken(token)
} }
// IsUsernameExists 判断用户名是否存在, it will check the internal database and LDAP(if enabled)
func (us *UserService) IsUsernameExists(username string) bool {
u := &model.User{}
global.DB.Where("username = ?", username).First(u)
existsInLdap := AllService.LdapService.IsUsernameExists(username)
return u.Id != 0 || existsInLdap
}