Compare commits

...

12 Commits

Author SHA1 Message Date
lejianwen
dbf8b23b15 fix: Config watchConfig (#135)
---
Closes: #135
2025-02-10 10:13:34 +08:00
lejianwen
79a5dd53ae fix: User disabled can not work (#133)
---
Closes: #133
2025-02-10 10:13:15 +08:00
Tao Chen
8a5b20685c fix: When OIDC and LDAP work togethar (#132 #134)
* fix OIDC create user if LDAP enable

* `newUser.GroupId = 1` for ldap

* fix
2025-02-10 10:08:49 +08:00
lejianwen
5a9c972de0 docs: Readme 2025-02-09 21:13:01 +08:00
Tao Chen
fc0e67122d docs: add LDAP info (#130) 2025-02-09 19:36:31 +08:00
lejianwen
eb642f66ca docs: Readme 2025-02-07 18:14:12 +08:00
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
12 changed files with 345 additions and 181 deletions

126
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/gorm-v1.25.7-green"/>
<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"/>
</div>
@@ -19,7 +20,10 @@
- 登录
- 地址簿
- 群组
- 授权登录,支持`github`, `google``OIDC` 登录,支持`web后台`授权登录
- 授权登录
- 支持`github`, `google``OIDC` 登录,
- 支持`web后台`授权登录
- 支持`LDAP`(AD和OpenLDAP已测试), 如果API Server配置了LDAP
- i18n
- Web Admin
- 用户管理
@@ -28,6 +32,7 @@
- 标签管理
- 群组管理
- Oauth 管理
- 配置LDAP, 配置文件或者环境变量
- 登录日志
- 链接日志
- 文件传输日志
@@ -46,6 +51,7 @@
## 功能
### API 服务
基本实现了PC端基础的接口。支持Personal版本接口可以通过配置文件`rustdesk.personal`或环境变量`RUSTDESK_API_RUSTDESK_PERSONAL`来控制是否启用
@@ -70,23 +76,20 @@
* 使用前后端分离,提供用户友好的管理界面,主要用来管理和展示。前端代码在[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. 管理员界面
![web_admin](docs/web_admin.png)
2. 普通用户界面
![web_user](docs/web_admin_user.png)
右上角可以更改密码,可以切换语言,可以切换`白天/黑夜`模式
![web_resetpwd](docs/web_resetpwd.png)
3. 每个用户可以多个地址簿,也可以将地址簿共享给其他用户
4. 分组可以自定义,方便管理,暂时支持两种类型: `共享组``普通组`
5. 可以直接打开webclient方便使用也可以分享给游客游客可以直接通过webclient远程到设备
![web_webclient](docs/admin_webclient.png)
6. Oauth,支持了`Github`, `Google` 以及 `OIDC`, 需要创建一个`OAuth App`,然后配置到后台
![web_admin_oauth](docs/web_admin_oauth.png)
- 对于`Google``Github`, `Issuer``Scopes`不需要填写.
- 对于`OIDC`, `Issuer`是必须的。`Scopes`是可选的,默认为 `openid,profile,email`. 确保可以获取 `sub`,`email``preferred_username`
- `github oauth app``Settings`->`Developer settings`->`OAuth Apps`->`New OAuth App`
@@ -108,6 +111,7 @@
![rustdesk_command_advance](./docs/rustdesk_command_advance.png)
11. **LDAP 支持**, 当在API Server上设置了LDAP(已测试AD和LDAP),可以通过LDAP中的用户信息进行登录 https://github.com/lejianwen/rustdesk-api/issues/114 ,如果LDAP验证失败返回本地用户
### Web Client:
@@ -127,6 +131,7 @@
![api_swag](docs/api_swag.png)
### CLI
```bash
# 查看帮助
./apimain -h
@@ -181,48 +186,70 @@ proxy:
jwt:
key: ""
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 |
| RUSTDESK_API_LANG | 语言 | `en`,`zh-CN` |
| RUSTDESK_API_APP_WEB_CLIENT | 是否启用web-client; 1:启用,0:不启用; 默认启用 | 1 |
| RUSTDESK_API_APP_REGISTER | 是否开启注册; `true`, `false` 默认`false` | `false` |
| RUSTDESK_API_APP_SHOW_SWAGGER | 是否可见swagger文档;`1`显示,`0`不显示,默认`0`不显示 | `1` |
| RUSTDESK_API_APP_TOKEN_EXPIRE | token有效时长 | `3600` |
| -----ADMIN配置----- | ---------- | ---------- |
| RUSTDESK_API_ADMIN_TITLE | 后台标题 | `RustDesk Api Admin` |
| RUSTDESK_API_ADMIN_HELLO | 后台欢迎语,可以使用`html` | |
| RUSTDESK_API_ADMIN_HELLO_FILE | 后台欢迎语文件,如果内容多,使用文件更方便。<br>会覆盖`RUSTDESK_API_ADMIN_HELLO` | `./conf/admin/hello.html` |
| -----GIN配置----- | ---------- | ---------- |
| RUSTDESK_API_GIN_TRUST_PROXY | 信任的代理IP列表`,`分割,默认信任所有 | 192.168.1.2,192.168.1.3 |
| -----------GORM配置---------------- | ------------------------------------ | --------------------------- |
| RUSTDESK_API_GORM_TYPE | 数据库类型sqlite或者mysql默认sqlite | sqlite |
| RUSTDESK_API_GORM_MAX_IDLE_CONNS | 数据库最大空闲连接数 | 10 |
| RUSTDESK_API_GORM_MAX_OPEN_CONNS | 数据库最大打开连接数 | 100 |
| RUSTDESK_API_RUSTDESK_PERSONAL | 是否启用个人版API 1:启用,0:不启用; 默认启用 | 1 |
| -----MYSQL配置----- | ---------- | ---------- |
| RUSTDESK_API_MYSQL_USERNAME | mysql用户名 | root |
| RUSTDESK_API_MYSQL_PASSWORD | mysql密码 | 111111 |
| RUSTDESK_API_MYSQL_ADDR | mysql地址 | 192.168.1.66:3306 |
| RUSTDESK_API_MYSQL_DBNAME | mysql数据库名 | rustdesk |
| -----RUSTDESK配置----- | --------------- | ---------- |
| 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_API_SERVER | Rustdesk的api服务器地址 | http://192.168.1.66:21114 |
| RUSTDESK_API_RUSTDESK_KEY | Rustdesk的key | 123456789 |
| RUSTDESK_API_RUSTDESK_KEY_FILE | Rustdesk存放key的文件 | `./conf/data/id_ed25519.pub` |
| RUSTDESK_API_RUSTDESK_WEBCLIENT_MAGIC_QUERYONLINE | Web client v2 中是否启用新的在线状态查询方法; `1`:启用,`0`:不启用,默认不启用 | `0` |
| ----PROXY配置----- | --------------- | ---------- |
| RUSTDESK_API_PROXY_ENABLE | 是否启用代理:`false`, `true` | `false` |
| RUSTDESK_API_PROXY_HOST | 代理地址 | `http://127.0.0.1:1080` |
| ----JWT配置---- | -------- | -------- |
| RUSTDESK_API_JWT_KEY | 自定义JWT KEY,为空则不启用JWT | |
| RUSTDESK_API_JWT_EXPIRE_DURATION | JWT有效时间 | 360000 |
| 变量名 | 说明 | 示例 |
|--------------------------------------------------------|--------------------------------------------------------------------------------|------------------------------|
| TZ | 时区 | Asia/Shanghai |
| RUSTDESK_API_LANG | 语言 | `en`,`zh-CN` |
| RUSTDESK_API_APP_WEB_CLIENT | 是否启用web-client; 1:启用,0:不启用; 默认启用 | 1 |
| RUSTDESK_API_APP_REGISTER | 是否开启注册; `true`, `false` 默认`false` | `false` |
| RUSTDESK_API_APP_SHOW_SWAGGER | 是否可见swagger文档;`1`显示,`0`不显示,默认`0`不显示 | `1` |
| RUSTDESK_API_APP_TOKEN_EXPIRE | token有效时长 | `3600` |
| -----ADMIN配置----- | ---------- | ---------- |
| RUSTDESK_API_ADMIN_TITLE | 后台标题 | `RustDesk Api Admin` |
| RUSTDESK_API_ADMIN_HELLO | 后台欢迎语,可以使用`html` | |
| RUSTDESK_API_ADMIN_HELLO_FILE | 后台欢迎语文件,如果内容多,使用文件更方便。<br>会覆盖`RUSTDESK_API_ADMIN_HELLO` | `./conf/admin/hello.html` |
| -----GIN配置----- | ---------- | ---------- |
| RUSTDESK_API_GIN_TRUST_PROXY | 信任的代理IP列表`,`分割,默认信任所有 | 192.168.1.2,192.168.1.3 |
| -----GORM配置----- | ---------- | --------------------------- |
| RUSTDESK_API_GORM_TYPE | 数据库类型sqlite或者mysql默认sqlite | sqlite |
| RUSTDESK_API_GORM_MAX_IDLE_CONNS | 数据库最大空闲连接数 | 10 |
| RUSTDESK_API_GORM_MAX_OPEN_CONNS | 数据库最大打开连接数 | 100 |
| RUSTDESK_API_RUSTDESK_PERSONAL | 是否启用个人版API 1:启用,0:不启用; 默认启用 | 1 |
| -----MYSQL配置----- | ---------- | ---------- |
| RUSTDESK_API_MYSQL_USERNAME | mysql用户名 | root |
| RUSTDESK_API_MYSQL_PASSWORD | mysql密码 | 111111 |
| RUSTDESK_API_MYSQL_ADDR | mysql地址 | 192.168.1.66:3306 |
| RUSTDESK_API_MYSQL_DBNAME | mysql数据库名 | rustdesk |
| -----RUSTDESK配置----- | ---------- | ---------- |
| 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_API_SERVER | Rustdesk的api服务器地址 | http://192.168.1.66:21114 |
| RUSTDESK_API_RUSTDESK_KEY | Rustdesk的key | 123456789 |
| RUSTDESK_API_RUSTDESK_KEY_FILE | Rustdesk存放key的文件 | `./conf/data/id_ed25519.pub` |
| RUSTDESK_API_RUSTDESK_WEBCLIENT<br/>_MAGIC_QUERYONLINE | Web client v2 中是否启用新的在线状态查询方法; `1`:启用,`0`:不启用,默认不启用 | `0` |
| ----PROXY配置----- | ---------- | ---------- |
| RUSTDESK_API_PROXY_ENABLE | 是否启用代理:`false`, `true` | `false` |
| RUSTDESK_API_PROXY_HOST | 代理地址 | `http://127.0.0.1:1080` |
| ----JWT配置---- | -------- | -------- |
| RUSTDESK_API_JWT_KEY | 自定义JWT KEY,为空则不启用JWT<br/>如果没使用`lejianwen/rustdesk-server`中的`MUST_LOGIN`,建议设置为空 | |
| RUSTDESK_API_JWT_EXPIRE_DURATION | JWT有效时间 | 360000 |
### 运行
@@ -288,10 +315,11 @@ jwt:
6. 打开浏览器访问`http://<your server[:port]>/_admin/`,默认用户名密码为`admin`,请及时更改密码。
#### 使用我fork后的server-s6镜像运行
#### 使用`lejianwen/server-s6`镜像运行
- 已解决链接超时问题
- 可以强制登录后才能发起链接
- github https://github.com/lejianwen/rustdesk-server
- docker hub https://hub.docker.com/r/lejianwen/rustdesk-server-s6
```yaml
networks:
@@ -344,4 +372,4 @@ jwt:
<img src="https://contrib.rocks/image?repo=lejianwen/rustdesk-api" />
</a>
## 感谢你的支持!如果这个项目对你有帮助,请点个⭐️鼓励一下,谢谢!

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/gorm-v1.25.7-green"/>
<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"/>
</div>
@@ -18,7 +19,10 @@ desktop software that provides self-hosted solutions.
- Login
- Address Book
- Groups
- Authorized login, supports `GitHub`, `Google` and `OIDC` login, supports `web admin` authorized login
- Authorized login,
- supports `GitHub`, `Google` and `OIDC` login,
- supports `web admin` authorized login,
- supports LDAP(test AD and openladp) if API Server config
- i18n
- Web Admin
- User Management
@@ -27,6 +31,7 @@ desktop software that provides self-hosted solutions.
- Tag Management
- Group Management
- OAuth Management
- LDAP Config by config file or ENV
- Login Logs
- Connection Logs
- File Transfer Logs
@@ -69,23 +74,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
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
installation are `admin` `admin`, please change the password immediately.
* Admin panel URL: `http://<your server[:port]>/_admin/`
* 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:
![web_admin](docs/en_img/web_admin.png)
2. Regular user interface:
![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.
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.
![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
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 `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`
@@ -97,19 +101,18 @@ installation are `admin` `admin`, please change the password immediately.
8. Connection logs
9. File transfer logs
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
![rustdesk_command_simple](./docs/en_img/rustdesk_command_simple.png)
- `Advanced mode`, commands can be executed directly in the backend
- `Advanced mode`, commands can be executed directly in the backend
* Official commands can be used
* Custom commands can be added
* 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)
11. **LDAP Support**, When you setup the LDAP(test for OpenLDAP and AD), you can login with the LDAP's user. https://github.com/lejianwen/rustdesk-api/issues/114 , if LDAP fail fallback local user
### Web Client:
1. If you're already logged into the admin panel, the web client will log in automatically.
@@ -182,48 +185,70 @@ proxy:
jwt:
key: ""
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
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 |
|---------------------------------------------------|--------------------------------------------------------------------------------------------------------------|-------------------------------|
| TZ | timezone | Asia/Shanghai |
| 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_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_TOKEN_EXPIRE | token expire duration(second) | `3600` |
| ----- ADMIN Configuration----- | ---------- | ---------- |
| RUSTDESK_API_ADMIN_TITLE | Admin Title | `RustDesk Api Admin` |
| 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` |
| ----- GIN Configuration ----- | --------------------------------------- | ----------------------------- |
| RUSTDESK_API_GIN_TRUST_PROXY | Trusted proxy IPs, separated by commas. | 192.168.1.2,192.168.1.3 |
| ----- GORM Configuration ----- | --------------------------------------- | ----------------------------- |
| 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_OPEN_CONNS | Maximum open connections | 100 |
| RUSTDESK_API_RUSTDESK_PERSONAL | Open Personal Api 1:Enable,0:Disable | 1 |
| ----- MYSQL Configuration ----- | --------------------------------------- | ----------------------------- |
| RUSTDESK_API_MYSQL_USERNAME | MySQL username | root |
| RUSTDESK_API_MYSQL_PASSWORD | MySQL password | 111111 |
| RUSTDESK_API_MYSQL_ADDR | MySQL address | 192.168.1.66:3306 |
| RUSTDESK_API_MYSQL_DBNAME | MySQL database name | rustdesk |
| ----- RUSTDESK Configuration ----- | --------------------------------------- | ----------------------------- |
| 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_API_SERVER | Rustdesk API server address | http://192.168.1.66:21114 |
| RUSTDESK_API_RUSTDESK_KEY | Rustdesk key | 123456789 |
| 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` |
| ---- PROXY ----- | --------------- | ---------- |
| RUSTDESK_API_PROXY_ENABLE | proxy_enable :`false`, `true` | `false` |
| RUSTDESK_API_PROXY_HOST | proxy_host | `http://127.0.0.1:1080` |
| ----JWT---- | -------- | -------- |
| RUSTDESK_API_JWT_KEY | JWT KEY. Set empty to disable jwt | |
| RUSTDESK_API_JWT_EXPIRE_DURATION | JWT expire duration | 360000 |
| Variable Name | Description | Example |
|---------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------|
| TZ | timezone | Asia/Shanghai |
| 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_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_TOKEN_EXPIRE | token expire duration(second) | `3600` |
| ----- ADMIN Configuration----- | ---------- | ---------- |
| RUSTDESK_API_ADMIN_TITLE | Admin Title | `RustDesk Api Admin` |
| 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` |
| ----- GIN Configuration ----- | --------------------------------------- | ----------------------------- |
| RUSTDESK_API_GIN_TRUST_PROXY | Trusted proxy IPs, separated by commas. | 192.168.1.2,192.168.1.3 |
| ----- GORM Configuration ----- | --------------------------------------- | ----------------------------- |
| 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_OPEN_CONNS | Maximum open connections | 100 |
| RUSTDESK_API_RUSTDESK_PERSONAL | Open Personal Api 1:Enable,0:Disable | 1 |
| ----- MYSQL Configuration ----- | --------------------------------------- | ----------------------------- |
| RUSTDESK_API_MYSQL_USERNAME | MySQL username | root |
| RUSTDESK_API_MYSQL_PASSWORD | MySQL password | 111111 |
| RUSTDESK_API_MYSQL_ADDR | MySQL address | 192.168.1.66:3306 |
| RUSTDESK_API_MYSQL_DBNAME | MySQL database name | rustdesk |
| ----- RUSTDESK Configuration ----- | --------------------------------------- | ----------------------------- |
| 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_API_SERVER | Rustdesk API server address | http://192.168.1.66:21114 |
| RUSTDESK_API_RUSTDESK_KEY | Rustdesk key | 123456789 |
| RUSTDESK_API_RUSTDESK_KEY_FILE | Rustdesk key file | `./conf/data/id_ed25519.pub` |
| RUSTDESK_API_RUSTDESK<br/>_WEBCLIENT_MAGIC_QUERYONLINE | New online query method is enabled in the web client v2; '1': Enabled, '0': Disabled, not enabled by default | `0` |
| ---- PROXY ----- | --------------- | ---------- |
| RUSTDESK_API_PROXY_ENABLE | proxy_enable :`false`, `true` | `false` |
| RUSTDESK_API_PROXY_HOST | proxy_host | `http://127.0.0.1:1080` |
| ----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 |
### Installation Steps
@@ -294,8 +319,9 @@ Download the release from [release](https://github.com/lejianwen/rustdesk-api/re
#### 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
- docker hub https://hub.docker.com/r/lejianwen/rustdesk-server-s6
```yaml
networks:
@@ -344,3 +370,5 @@ Thanks to everyone who contributed!
<a href="https://github.com/lejianwen/rustdesk-api/graphs/contributors">
<img src="https://contrib.rocks/image?repo=lejianwen/rustdesk-api" />
</a>
## Thanks for your support! If you find this project useful, please give it a ⭐️. Thank you!

View File

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

View File

@@ -4,6 +4,7 @@ app:
register: false #是否开启注册
show-swagger: 0 # 1:启用 0:禁用
token-expire: 360000
web-sso: true #web auth sso
admin:
title: "RustDesk Api Admin"
hello-file: "./conf/admin/hello.html" #优先使用file
@@ -40,6 +41,27 @@ proxy:
jwt:
key: ""
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:
addr: "127.0.0.1:6379"
password: ""
@@ -58,23 +80,3 @@ oss:
expire-time: 30
max-byte: 10240
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.

View File

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

BIN
docs/init_admin_pwd.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View File

@@ -182,15 +182,20 @@ func (ct *Login) Login(c *gin.Context) {
global.Logger.Warn(fmt.Sprintf("Login Fail: %s %s %s", "UsernameOrPasswordError", c.RemoteIP(), clientIp))
loginLimiter.RecordFailure(clientIp)
if loginLimiter.NeedsCaptcha(clientIp) {
// 移除原验证码,重新生成
loginLimiter.RemoveCaptcha(clientIp)
response.Fail(c, 110, response.TranslateMsg(c, "UsernameOrPasswordError"))
return
}
response.Fail(c, 101, response.TranslateMsg(c, "UsernameOrPasswordError"))
return
}
if !service.AllService.UserService.CheckUserEnable(u) {
if loginLimiter.NeedsCaptcha(clientIp) {
loginLimiter.RemoveCaptcha(clientIp)
}
response.Fail(c, 101, response.TranslateMsg(c, "UserDisabled"))
return
}
ut := service.AllService.UserService.Login(u, &model.LoginLog{
UserId: u.Id,
Client: model.LoginLogClientWebAdmin,

View File

@@ -51,6 +51,11 @@ func (l *Login) Login(c *gin.Context) {
return
}
if !service.AllService.UserService.CheckUserEnable(u) {
response.Error(c, response.TranslateMsg(c, "UserDisabled"))
return
}
//根据refer判断是webclient还是app
ref := c.GetHeader("referer")
if ref != "" {
@@ -85,7 +90,9 @@ func (l *Login) Login(c *gin.Context) {
// @Router /login-options [get]
func (l *Login) LoginOptions(c *gin.Context) {
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
for _, v := range ops {
oidcItems = append(oidcItems, map[string]string{"name": v})

View File

@@ -24,6 +24,14 @@ func AdminAuth() gin.HandlerFunc {
return
}
if !service.AllService.UserService.CheckUserEnable(user) {
c.JSON(401, gin.H{
"error": "Unauthorized",
})
c.Abort()
return
}
c.Set("curUser", user)
c.Set("token", token)
//如果时间小于1天,token自动续期

View File

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

View File

@@ -4,15 +4,30 @@ import (
"crypto/tls"
"errors"
"fmt"
"github.com/go-ldap/ldap/v3"
"strconv"
"strings"
"github.com/go-ldap/ldap/v3"
"Gwen/config"
"Gwen/global"
"Gwen/model"
)
var (
ErrLdapNotEnabled = errors.New("LdapNotEnabled")
ErrLdapUserDisabled = errors.New("UserDisabledAtLdap")
ErrLdapUserNotFound = errors.New("UserNotFound")
ErrLdapMailNotMatch = errors.New("MailNotMatch")
ErrLdapConnectFailed = errors.New("LdapConnectFailed")
ErrLdapSearchFailed = errors.New("LdapSearchRequestFailed")
ErrLdapTlsFailed = errors.New("LdapStartTLSFailed")
ErrLdapBindService = errors.New("LdapBindServiceFailed")
ErrLdapBindFailed = errors.New("LdapBindFailed")
ErrLdapToLocalUserFailed = errors.New("LdapToLocalUserFailed")
ErrLdapCreateUserFailed = errors.New("LdapCreateUserFailed")
)
// LdapService is responsible for LDAP authentication and user synchronization.
type LdapService struct {
}
@@ -43,6 +58,11 @@ func (lu *LdapUser) ToUser(u *model.User) *model.User {
u.Username = lu.Username
u.Email = lu.Email
u.Nickname = lu.Name()
if lu.Enabled {
u.Status = model.COMMON_STATUS_ENABLE
} else {
u.Status = model.COMMON_STATUS_DISABLED
}
return u
}
@@ -50,21 +70,21 @@ func (lu *LdapUser) ToUser(u *model.User) *model.User {
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)
return nil, errors.Join(ErrLdapConnectFailed, 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)
return nil, errors.Join(ErrLdapTlsFailed, 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 nil, errors.Join(ErrLdapBindService, err)
}
return conn, nil
}
@@ -87,29 +107,17 @@ func (ls *LdapService) verifyCredentials(cfg *config.Ldap, username, password st
// 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)
ldapUser, err := ls.GetUserInfoByUsernameLdap(username)
if err != nil {
return nil, fmt.Errorf("LDAP search request failed: %w", err)
return nil, 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")
return nil, ErrLdapUserDisabled
}
cfg := &global.Config.Ldap
user, err := ls.mapToLocalUser(cfg, ldapUser)
if err != nil {
return nil, fmt.Errorf("failed to map LDAP user to local user: %w", err)
return nil, errors.Join(ErrLdapToLocalUserFailed, err)
}
return user, nil
}
@@ -126,8 +134,9 @@ func (ls *LdapService) mapToLocalUser(cfg *config.Ldap, lu *LdapUser) (*model.Us
// Typically, you dont store LDAP user passwords locally.
// If needed, you can set a random password here.
newUser.IsAdmin = &isAdmin
newUser.GroupId = 1
if err := global.DB.Create(newUser).Error; err != nil {
return nil, fmt.Errorf("failed to create new user: %w", err)
return nil, errors.Join(ErrLdapCreateUserFailed, err)
}
return userService.InfoByUsername(lu.Username), nil
}
@@ -137,6 +146,7 @@ func (ls *LdapService) mapToLocalUser(cfg *config.Ldap, lu *LdapUser) (*model.Us
originalEmail := localUser.Email
originalNickname := localUser.Nickname
originalIsAdmin := localUser.IsAdmin
originalStatus := localUser.Status
lu.ToUser(localUser) // merges LDAP data into the existing user
localUser.IsAdmin = &isAdmin
if err := userService.Update(localUser); err != nil {
@@ -144,6 +154,7 @@ func (ls *LdapService) mapToLocalUser(cfg *config.Ldap, lu *LdapUser) (*model.Us
localUser.Email = originalEmail
localUser.Nickname = originalNickname
localUser.IsAdmin = originalIsAdmin
localUser.Status = originalStatus
}
}
@@ -177,6 +188,56 @@ func (ls *LdapService) IsEmailExists(email string) bool {
return len(sr.Entries) > 0
}
// GetUserInfoByUsernameLdap returns the user info from LDAP for the given username.
func (ls *LdapService) GetUserInfoByUsernameLdap(username string) (*LdapUser, error) {
cfg := &global.Config.Ldap
if !cfg.Enable {
return nil, ErrLdapNotEnabled
}
sr, err := ls.usernameSearchResult(cfg, username)
if err != nil {
return nil, errors.Join(ErrLdapSearchFailed, err)
}
if len(sr.Entries) != 1 {
return nil, ErrLdapUserNotFound
}
return ls.userResultToLdapUser(cfg, sr.Entries[0]), nil
}
// GetUserInfoByUsernameLocal returns the user info from LDAP for the given username. If the user exists, it will sync the user info to the local database.
func (ls *LdapService) GetUserInfoByUsernameLocal(username string) (*model.User, error) {
ldapUser, err := ls.GetUserInfoByUsernameLdap(username)
if err != nil {
return &model.User{}, err
}
return ls.mapToLocalUser(&global.Config.Ldap, ldapUser)
}
// GetUserInfoByEmailLdap returns the user info from LDAP for the given email.
func (ls *LdapService) GetUserInfoByEmailLdap(email string) (*LdapUser, error) {
cfg := &global.Config.Ldap
if !cfg.Enable {
return nil, ErrLdapNotEnabled
}
sr, err := ls.emailSearchResult(cfg, email)
if err != nil {
return nil, errors.Join(ErrLdapSearchFailed, err)
}
if len(sr.Entries) != 1 {
return nil, ErrLdapUserNotFound
}
return ls.userResultToLdapUser(cfg, sr.Entries[0]), nil
}
// GetUserInfoByEmailLocal returns the user info from LDAP for the given email. if the user exists, it will synchronize the user information to local database.
func (ls *LdapService) GetUserInfoByEmailLocal(email string) (*model.User, error) {
ldapUser, err := ls.GetUserInfoByEmailLdap(email)
if err != nil {
return &model.User{}, err
}
return ls.mapToLocalUser(&global.Config.Ldap, ldapUser)
}
// 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

View File

@@ -5,12 +5,13 @@ import (
"Gwen/model"
"Gwen/utils"
"errors"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"math/rand"
"strconv"
"strings"
"time"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
type UserService struct {
@@ -322,7 +323,16 @@ func (us *UserService) RegisterByOauth(oauthUser *model.OauthUser, op string) (e
email = strings.ToLower(email)
// update email to oauthUser, in case it contain upper case
oauthUser.Email = email
user := us.InfoByEmail(email)
// call this, if find user by email, it will update the email to local database
user, ldapErr := AllService.LdapService.GetUserInfoByEmailLocal(email)
// If we enable ldap, and the error is not ErrLdapUserNotFound, return the error because we could not sure if the user is not found in ldap
if !(errors.Is(ldapErr, ErrLdapNotEnabled) || errors.Is(ldapErr, ErrLdapUserNotFound) || ldapErr == nil) {
return ldapErr, user
}
if user.Id == 0 {
// this means the user is not found in ldap, maybe ldao is not enabled
user = us.InfoByEmail(email)
}
if user.Id != 0 {
ut.FromOauthUser(user.Id, oauthUser, oauthType, op)
global.DB.Create(ut)
@@ -491,8 +501,15 @@ func (us *UserService) VerifyJWT(token string) (uint, error) {
// IsUsernameExists 判断用户名是否存在, it will check the internal database and LDAP(if enabled)
func (us *UserService) IsUsernameExists(username string) bool {
return us.IsUsernameExistsLocal(username) || AllService.LdapService.IsUsernameExists(username)
}
func (us *UserService) IsUsernameExistsLocal(username string) bool {
u := &model.User{}
global.DB.Where("username = ?", username).First(u)
existsInLdap := AllService.LdapService.IsUsernameExists(username)
return u.Id != 0 || existsInLdap
return u.Id != 0
}
func (us *UserService) IsEmailExistsLdap(email string) bool {
return AllService.LdapService.IsEmailExists(email)
}