Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8cac15f7dd | ||
|
|
5011e2b7c1 | ||
|
|
b0008143b1 | ||
|
|
a3c3ab5a72 | ||
|
|
3a16269215 | ||
|
|
151145b0c3 | ||
|
|
9c794e9d4b | ||
|
|
01f697d279 | ||
|
|
6cdc37333b | ||
|
|
ae32915565 |
1
.github/workflows/build.yml
vendored
1
.github/workflows/build.yml
vendored
@@ -203,6 +203,7 @@ jobs:
|
||||
- name: Build package for ${{ matrix.job.platform }} arch
|
||||
run: |
|
||||
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/*
|
||||
mkdir -p data
|
||||
cp -vr debian systemd conf data resources runtime debian-build/${{ matrix.job.platform }}/
|
||||
|
||||
119
README.md
119
README.md
@@ -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>
|
||||
|
||||
@@ -46,6 +47,7 @@
|
||||
|
||||
## 功能
|
||||
|
||||
|
||||
### API 服务
|
||||
基本实现了PC端基础的接口。支持Personal版本接口,可以通过配置文件`rustdesk.personal`或环境变量`RUSTDESK_API_RUSTDESK_PERSONAL`来控制是否启用
|
||||
|
||||
@@ -70,23 +72,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)更改密码
|
||||
|
||||

|
||||
|
||||
1. 管理员界面
|
||||

|
||||
2. 普通用户界面
|
||||

|
||||
|
||||
右上角可以更改密码,可以切换语言,可以切换`白天/黑夜`模式
|
||||
|
||||

|
||||
|
||||
3. 每个用户可以多个地址簿,也可以将地址簿共享给其他用户
|
||||
4. 分组可以自定义,方便管理,暂时支持两种类型: `共享组` 和 `普通组`
|
||||
5. 可以直接打开webclient,方便使用;也可以分享给游客,游客可以直接通过webclient远程到设备
|
||||

|
||||
6. Oauth,支持了`Github`, `Google` 以及 `OIDC`, 需要创建一个`OAuth App`,然后配置到后台
|
||||

|
||||
- 对于`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`
|
||||
@@ -127,6 +126,7 @@
|
||||

|
||||
|
||||
### CLI
|
||||
|
||||
```bash
|
||||
# 查看帮助
|
||||
./apimain -h
|
||||
@@ -181,48 +181,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_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 +310,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 +367,4 @@ jwt:
|
||||
<img src="https://contrib.rocks/image?repo=lejianwen/rustdesk-api" />
|
||||
</a>
|
||||
|
||||
|
||||
## 如果觉得这个项目对你有帮助,请给一个star,谢谢!
|
||||
|
||||
124
README_EN.md
124
README_EN.md
@@ -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>
|
||||
|
||||
@@ -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
|
||||
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).
|
||||
|
||||

|
||||
|
||||
|
||||
1. Admin interface:
|
||||

|
||||
2. Regular user interface:
|
||||

|
||||
In the top right corner, you can change the password, switch languages, and toggle between `day/night` mode.
|
||||
|
||||

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

|
||||
6. OAuth support: Currently, `GitHub`, `Google` and `OIDC` are supported. You need to create an `OAuth App` and configure it in
|
||||
the admin panel.
|
||||

|
||||
- 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,16 +97,15 @@ 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
|
||||

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

|
||||

|
||||
|
||||
|
||||
|
||||
@@ -182,48 +181,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_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 +315,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 +366,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>
|
||||
|
||||
## If you find this project helpful, please give it a star, thank you!
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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: ""
|
||||
|
||||
@@ -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,6 +38,7 @@ type Config struct {
|
||||
Jwt Jwt
|
||||
Rustdesk Rustdesk
|
||||
Proxy Proxy
|
||||
Ldap Ldap
|
||||
}
|
||||
|
||||
// Init 初始化配置
|
||||
@@ -56,15 +57,19 @@ func Init(rowVal *Config, path string) *viper.Viper {
|
||||
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.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)
|
||||
}
|
||||
|
||||
36
config/ldap.go
Normal file
36
config/ldap.go
Normal 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
BIN
docs/init_admin_pwd.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 19 KiB |
13
go.mod
13
go.mod
@@ -13,7 +13,7 @@ require (
|
||||
github.com/go-playground/validator/v10 v10.11.2
|
||||
github.com/go-redis/redis/v8 v8.11.4
|
||||
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/sirupsen/logrus 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/swag v1.16.3
|
||||
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/sqlite v1.5.6
|
||||
gorm.io/gorm v1.25.7
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
|
||||
github.com/KyleBanks/depth v1.2.1 // indirect
|
||||
github.com/PuerkitoBio/purell v1.1.1 // 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/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // 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/jsonreference v0.19.6 // 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/ugorji/go/codec v1.2.9 // 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/net v0.25.0 // indirect
|
||||
golang.org/x/sys v0.25.0 // indirect
|
||||
golang.org/x/net v0.33.0 // indirect
|
||||
golang.org/x/sys v0.28.0 // indirect
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
|
||||
google.golang.org/protobuf v1.33.0 // indirect
|
||||
gopkg.in/ini.v1 v1.63.2 // indirect
|
||||
|
||||
@@ -85,7 +85,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})
|
||||
|
||||
@@ -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"},
|
||||
|
||||
10
resources/web2/js/dist/index.js
vendored
10
resources/web2/js/dist/index.js
vendored
@@ -9649,7 +9649,7 @@ const hn = async (u, e) => {
|
||||
o = !1
|
||||
}
|
||||
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 {
|
||||
constructor() {
|
||||
@@ -11087,7 +11087,7 @@ function R4(u = !1) {
|
||||
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 [domain, uriport] = uri.split(":")
|
||||
if (isHttps() && (!uriport)) {
|
||||
@@ -11096,7 +11096,7 @@ function getrUriFromRs(uri, isRelay = false, roffset = 0) {
|
||||
if (uriport) {
|
||||
const port = parseInt(uriport);
|
||||
uri = domain + ":" + (port + (isRelay ? roffset || 3 : 2))
|
||||
} else uri += ":" + (Ut + (isRelay ? 3 : 2));
|
||||
} else uri += ":" + (defaultIdServerPort + (isRelay ? 3 : 2));
|
||||
return p + uri
|
||||
}
|
||||
|
||||
@@ -11105,7 +11105,7 @@ function isHttps() {
|
||||
}
|
||||
|
||||
function I4(u, e = !1, i = 0) {
|
||||
return getrUriFromRs(u, e, i)
|
||||
return getUriFromRs(u, e, i)
|
||||
}
|
||||
|
||||
function wn() {
|
||||
@@ -11239,7 +11239,7 @@ async function Pn(u) {
|
||||
o.close(), console.error("Failed to query online states, no online response")
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
const Rn = "rustdesk-client";
|
||||
|
||||
425
service/ldap.go
Normal file
425
service/ldap.go
Normal 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 don’t 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)
|
||||
}
|
||||
@@ -18,6 +18,7 @@ type Service struct {
|
||||
*AuditService
|
||||
*ShareRecordService
|
||||
*ServerCmdService
|
||||
*LdapService
|
||||
}
|
||||
|
||||
func New() *Service {
|
||||
|
||||
@@ -46,6 +46,14 @@ func (us *UserService) InfoByOpenid(openid string) *model.User {
|
||||
|
||||
// InfoByUsernamePassword 根据用户名密码取用户信息
|
||||
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{}
|
||||
global.DB.Where("username = ? and password = ?", username, us.EncryptPassword(password)).First(u)
|
||||
return u
|
||||
@@ -82,7 +90,7 @@ func (us *UserService) Login(u *model.User, llog *model.LoginLog) *model.UserTok
|
||||
Token: token,
|
||||
DeviceUuid: llog.Uuid,
|
||||
DeviceId: llog.DeviceId,
|
||||
ExpiredAt: time.Now().Add(time.Second * time.Duration(global.Config.App.TokenExpire)).Unix(),
|
||||
ExpiredAt: us.UserTokenExpireTimestamp(),
|
||||
}
|
||||
global.DB.Create(ut)
|
||||
llog.UserTokenId = ut.UserId
|
||||
@@ -156,6 +164,9 @@ func (us *UserService) CheckUserEnable(u *model.User) bool {
|
||||
// Create 创建
|
||||
func (us *UserService) Create(u *model.User) error {
|
||||
// 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.Password = us.EncryptPassword(u.Password)
|
||||
res := global.DB.Create(u).Error
|
||||
@@ -343,13 +354,10 @@ func (us *UserService) RegisterByOauth(oauthUser *model.OauthUser, op string) (e
|
||||
|
||||
// GenerateUsernameByOauth 生成用户名
|
||||
func (us *UserService) GenerateUsernameByOauth(name string) string {
|
||||
u := &model.User{}
|
||||
global.DB.Where("username = ?", name).First(u)
|
||||
if u.Id == 0 {
|
||||
return name
|
||||
for us.IsUsernameExists(name) {
|
||||
name += strconv.Itoa(rand.Intn(10)) // Append a random digit (0-9)
|
||||
}
|
||||
name = name + strconv.FormatInt(rand.Int63n(10), 10)
|
||||
return us.GenerateUsernameByOauth(name)
|
||||
return name
|
||||
}
|
||||
|
||||
// UserThirdsByUserId
|
||||
@@ -394,15 +402,18 @@ func (us *UserService) IsPasswordEmptyByUser(u *model.User) bool {
|
||||
return us.IsPasswordEmptyById(u.Id)
|
||||
}
|
||||
|
||||
// Register 注册
|
||||
// Register 注册, 如果用户名已存在则返回nil
|
||||
func (us *UserService) Register(username string, email string, password string) *model.User {
|
||||
u := &model.User{
|
||||
Username: username,
|
||||
Email: email,
|
||||
Password: us.EncryptPassword(password),
|
||||
Password: password,
|
||||
GroupId: 1,
|
||||
}
|
||||
global.DB.Create(u)
|
||||
err := us.Create(u)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return u
|
||||
}
|
||||
|
||||
@@ -451,8 +462,17 @@ func (us *UserService) getAdminUserCount() int64 {
|
||||
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) {
|
||||
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)
|
||||
}
|
||||
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) {
|
||||
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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user