12 Commits

Author SHA1 Message Date
GL.iNet-Yongping.Xie b29ca0c117 doc: update Chinese and English README for LDAP login
1. Updated both Chinese and English README files to document LDAP login
   support.

Signed-off-by: GL.iNet-Yongping.Xie <yongping.xie@gl-inet.com>
2025-09-30 04:12:49 -07:00
iclannad 7ce942d0d3 Merge pull request #8 from CU-Jon/feature/LDAP
feat: add optional LDAP authentication support
2025-09-30 18:39:16 +08:00
Jon Agramonte 5e16e10e9a feat: add optional LDAP authentication support
- Introduced LDAP authentication configuration options in the README and Docker Compose files.
- Updated API to handle dual authentication methods (LDAP and legacy).
- Implemented LDAP authentication logic in a new ldap.go file.
- Enhanced user login flow to support username and password for LDAP.
- Added error handling for authorization and authentication failures.
- Updated UI to include username input when LDAP is enabled and provide authentication options.
- Added localization for new authentication messages in English and Chinese.
2025-09-29 09:59:14 -04:00
GL.iNet-Yongping.Xie 3e9e5bb93a feat: support multi-level subdomain certificates
1. Added support for multi-level subdomains and certificates, such as
   *.example.com, *.level1.example.com, and *.level2.level1.example.com.
2. Enabled remote device access through domains like
   devId.example.com, devId.level1.example.com, and
   devId.level2.level1.example.com.

Signed-off-by: GL.iNet-Yongping.Xie <yongping.xie@gl-inet.com>
2025-09-26 02:29:17 -07:00
pengyu.lu 3c6c214479 fix: Fixed the issue where the gl/main dependency could not be downloaded 2025-09-25 11:40:26 +08:00
GL.iNet-Yongping.Xie ed3da45ddf feat: release version v1.0.1
1. Published official Docker images to Docker Hub.
2. Added docker-compose configuration template for easier deployment.

Signed-off-by: GL.iNet-Yongping.Xie <yongping.xie@gl-inet.com>
2025-09-15 21:00:58 -07:00
GL.iNet-Yongping.Xie b089169270 doc: update docker-compose YAML file
1. Official image has been pushed to Docker Hub.
2. Updated the docker-compose YAML file to reference the official image.

Signed-off-by: GL.iNet-Yongping.Xie <yongping.xie@gl-inet.com>
2025-09-15 19:17:00 -07:00
GL.iNet-Yongping.Xie 2349554c22 doc: update docker-compose startup instructions
1. Updated the documentation for starting services with docker-compose,
   making the description clearer and more accurate.

Signed-off-by: GL.iNet-Yongping.Xie <yongping.xie@gl-inet.com>
2025-09-15 01:24:49 -07:00
GL.iNet-Yongping.Xie 77886cf052 doc: update docker-compose startup instructions
1. Updated the documentation for starting services with docker-compose,
   making the description clearer and more accurate.

Signed-off-by: GL.iNet-Yongping.Xie <yongping.xie@gl-inet.com>
2025-09-15 01:19:29 -07:00
GL.iNet-Yongping.Xie 9be7a854ce feat: add initial docker-compose.yml for testing
1. Added support for docker-compose.yml to simplify deployment and
   service management.
2. Tested environment variable configurations to validate compatibility
   and ensure correct behavior.

Signed-off-by: GL.iNet-Yongping.Xie <yongping.xie@gl-inet.com>
2025-09-15 01:05:21 -07:00
GL.iNet-Yongping.Xie 2b6aecf72a fix: remove docker-compose.yml file
1. The glkvm-cloud image has not yet been pushed to the repository and
   cannot be used directly.
2. Temporarily removed the docker-compose.yml file to prevent customers
   from misusing it.

Signed-off-by: GL.iNet-Yongping.Xie <yongping.xie@gl-inet.com>
2025-09-10 01:08:44 -07:00
GL.iNet-Yongping.Xie efa73caf75 fix: update Chinese and English README
1. Added clarification about dedicated bandwidth for self-hosted
   deployments.
2. Corrected grammar errors in the English documentation.
3. Added server bandwidth configuration requirements to guide proper
   deployment.

Signed-off-by: GL.iNet-Yongping.Xie <yongping.xie@gl-inet.com>
2025-09-07 20:08:56 -07:00
62 changed files with 1797 additions and 822 deletions
+7 -3
View File
@@ -36,6 +36,10 @@ build-run: build run
# Build frontend, build Go binary, and run
full-run: ui build run
# Build Docker image
docker-build: ui build
docker build -t glkvm:v1 .
# Build Docker image without updating ui
docker-build: build
docker build -t glkvm-cloud:build .
# Full Build Docker image
docker-fullbuild: ui build
docker build -t glkvm-cloud:build .
+35 -4
View File
@@ -13,7 +13,9 @@ Self-Deployed Lightweight Cloud is a lightweight KVM remote cloud platform tailo
- **Batch Operations** - Batch command execution capabilities
- **Rapid Deployment** - Quick self-deployment with simple operations
- **Data Security** - Private deployment with full data control
- **Dedicated Bandwidth** - Exclusive bandwidth for self-hosted deployments
- **Lightweight Design** - Optimized for small businesses and individual users
- **Enterprise Authentication** - LDAP login support for enterprise users
## Self-Hosting Guide
@@ -34,9 +36,10 @@ The following mainstream operating systems have been tested and verified
| Component | Minimum Requirement |
| :-----------------: | :-----------------: |
| CPU | 1 cores or above |
| CPU | 1 core or above |
| Memory | ≥ 1 GB |
| Storage | ≥ 40 GB |
| Network Bandwidth | ≥ 3 Mbps |
| KVM Device Firmware | ≥ v1.5.0 |
#### 🔐 Cloud Security Group Settings
@@ -56,12 +59,22 @@ If your server provider uses a **cloud security group** (e.g., AWS, Aliyun, etc.
------
### 📦 Installation
Run the following command **as root** to install GLKVM Cloud:
We provide **two** ways to install GLKVM Cloud:
#### A) One-line installer (recommended)
> **Note:** The one-line installer is **Docker-based**. It automates Docker/Compose setup, pulls images, renders configs from templates, and starts services for you.
Run **as root**:
```bash
( command -v curl >/dev/null 2>&1 && curl -fsSL https://kvm-cloud.gl-inet.com/selfhost/install.sh || wget -qO- https://kvm-cloud.gl-inet.com/selfhost/install.sh ) | sudo bash
```
#### B) Docker manual install
> Full reference: see [`docker-compose/README.md`](https://github.com/gl-inet/glkvm-cloud/blob/main/docker-compose/README.md)
### 🌐 Platform Access
Once the installation is complete, access the platform via:
@@ -151,9 +164,27 @@ Replace the following files in:
⚠️ **Make sure the filenames remain unchanged.**
#### 🔄 Restart Services After Certificate Replacement
#### 🔐 LDAP Authentication Configuration (Optional)
After replacing the certificates, restart the GLKVM Cloud services to apply the changes:
GLKVM Cloud supports LDAP authentication for enterprise environments, allowing you to integrate with existing directory services like Active Directory, OpenLDAP, or FreeIPA.
**Key Features:**
- **Dual Authentication Mode**: Support both LDAP and traditional password authentication simultaneously
- **Group-based Authorization**: Restrict access to specific LDAP groups
- **User-based Authorization**: Allow access for specific users only
- **TLS/SSL Support**: Secure LDAP connections with encryption
- **Multiple LDAP Systems**: Compatible with Active Directory, OpenLDAP, FreeIPA, and generic LDAP servers
**Configuration:**
For detailed LDAP configuration options and setup instructions, see the [Docker Compose README](docker-compose/README.md).
**Note**: When LDAP is enabled, users can choose between:
- **LDAP Authentication**: Enter username and password for directory service authentication
- **Legacy Authentication**: Leave username empty and use the web management password
#### 🔄 Restart Services After Configuration Changes
After replacing certificates or updating LDAP configuration, restart the GLKVM Cloud services to apply the changes:
```bash
cd ~/glkvm_cloud
+24 -3
View File
@@ -14,7 +14,9 @@
* **批量操作** - 支持批量执行命令
* **快速部署** - 简单命令即可完成自部署
* **数据安全** - 私有化部署,数据完全可控
* **独享带宽** - 自部署环境下可享受专属带宽
* **轻量设计** - 专为小型企业和个人优化
* **企业级认证** - 支持 LDAP 登录,适用于企业用户
## 自部署指南
@@ -34,10 +36,11 @@
#### 系统要求
| 组件 | 最低配置要求 |
| ------------ | ------------ |
| :------------: | :------------: |
| CPU | 1 核及以上 |
| 内存 | ≥ 1 GB |
| 存储 | ≥ 40 GB |
| 网络带宽 | ≥ 3 Mbps |
| KVM 固件版本 | ≥ v1.5.0 |
#### 🔐 云安全组端口要求
@@ -147,9 +150,27 @@ Web UI 的默认登录密码会在安装脚本运行结束时显示:
* `glkvm.cer`
* `glkvm.key`
---
#### 🔐 LDAP 身份认证配置(可选)
#### 🔄 替换证书后重启服务
GLKVM 轻量云支持 LDAP 身份认证,适用于企业环境,可以与现有的目录服务(如 Active Directory、OpenLDAP 或 FreeIPA)集成。
**主要功能:**
- **双重认证模式**:同时支持 LDAP 和传统密码认证
- **基于组的授权**:限制特定 LDAP 组访问
- **基于用户的授权**:仅允许特定用户访问
- **TLS/SSL 支持**:加密 LDAP 连接
- **多 LDAP 系统支持**:兼容 Active Directory、OpenLDAP、FreeIPA 和通用 LDAP 服务器
**配置方法:**
详细的 LDAP 配置选项和设置说明,请参见 [Docker Compose README](docker-compose/README.md)。
**注意**:启用 LDAP 后,用户可以选择:
- **LDAP 认证**:输入用户名和密码进行目录服务认证
- **传统认证**:留空用户名并使用 Web 管理密码
#### 🔄 配置更改后重启服务
替换证书或更新 LDAP 配置后,需要重启 GLKVM 轻量云服务以应用更改:
```bash
cd ~/glkvm_cloud
+67 -30
View File
@@ -215,39 +215,64 @@ func (srv *RttyServer) ListenAPI() error {
c.Status(http.StatusOK)
})
r.POST("/signin", func(c *gin.Context) {
type credentials struct {
Password string `json:"password"`
}
r.POST("/signin", func(c *gin.Context) {
type credentials struct {
Username string `json:"username"`
Password string `json:"password"`
AuthMethod string `json:"authMethod"`
}
creds := credentials{}
creds := credentials{}
err := c.BindJSON(&creds)
if err != nil {
c.Status(http.StatusBadRequest)
return
}
err := c.BindJSON(&creds)
if err != nil {
c.Status(http.StatusBadRequest)
return
}
if httpLogin(cfg, creds.Password) {
sid := utils.GenUniqueID()
// 自动确定认证方法或使用指定的方法 (Auto-determine auth method or use specified method)
authMethod := creds.AuthMethod
if authMethod == "" {
// 基于是否提供用户名进行自动检测 (Auto-detect based on whether username is provided)
if creds.Username != "" && cfg.LdapEnabled {
authMethod = "ldap"
} else {
authMethod = "legacy"
}
}
httpSessions.Set(sid, true, cache.WithEx(httpSessionExpire))
success, errorType := AuthenticateUserWithError(cfg, creds.Username, creds.Password, authMethod)
if success {
sid := utils.GenUniqueID()
httpSessions.Set(sid, true, cache.WithEx(httpSessionExpire))
c.SetCookie("sid", sid, 0, "", "", false, true)
c.Status(http.StatusOK)
return
}
c.SetCookie("sid", sid, 0, "", "", false, true)
c.Status(http.StatusOK)
return
}
// 根据错误类型返回适当的错误信息 (Return appropriate error message based on error type)
if errorType == "authorization" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "user not authorized"})
} else {
c.JSON(http.StatusUnauthorized, gin.H{"error": "authentication failed"})
}
})
c.Status(http.StatusUnauthorized)
})
r.GET("/auth-config", func(c *gin.Context) {
authConfig := gin.H{
"ldapEnabled": cfg.LdapEnabled,
"legacyPassword": cfg.Password != "",
}
c.JSON(http.StatusOK, authConfig)
})
r.GET("/alive", func(c *gin.Context) {
if !httpAuth(cfg, c) {
c.AbortWithStatus(http.StatusUnauthorized)
} else {
c.Status(http.StatusOK)
}
})
r.GET("/alive", func(c *gin.Context) {
if !httpAuth(cfg, c) {
c.AbortWithStatus(http.StatusUnauthorized)
} else {
c.Status(http.StatusOK)
}
})
fs, err := fs.Sub(staticFs, "ui/dist")
if err != nil {
@@ -295,15 +320,23 @@ func (srv *RttyServer) ListenAPI() error {
host := c.Request.Host
hostname, _, err := net.SplitHostPort(host)
if err != nil {
// 没有端口时直接使用 host
hostname = host
hostname = host // Use host directly if no port
}
// Choose value by priority:
// 1) If request host is a domain (not an IP), keep it.
// 2) Else if it's an IP and cfg.WebrtcIP is set, use cfg.WebrtcIP.
// 3) Else keep the request IP.
chosen := hostname
if isIP(hostname) && cfg.WebrtcIP != "" {
chosen = cfg.WebrtcIP
}
c.JSON(http.StatusOK, gin.H{
"hostname": hostname,
"hostname": chosen, // reuse the same chosen value
"port": cfg.AddrDev,
"token": cfg.Token,
"webrtcIP": cfg.WebrtcIP,
"webrtcIP": chosen, // same as hostname
"webrtcPort": cfg.WebrtcPort,
"webrtcUsername": cfg.WebrtcUsername,
"webrtcPassword": cfg.WebrtcPassword,
@@ -332,6 +365,10 @@ func (srv *RttyServer) ListenAPI() error {
return r.RunListener(ln)
}
func isIP(addr string) bool {
return net.ParseIP(addr) != nil
}
func callUserHookUrl(cfg *Config, c *gin.Context) bool {
if cfg.UserHookUrl == "" {
return true
-27
View File
@@ -1,27 +0,0 @@
-----BEGIN CERTIFICATE-----
MIIEVzCCAj+gAwIBAgIRALBXPpFzlydw27SHyzpFKzgwDQYJKoZIhvcNAQELBQAw
TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMjQwMzEzMDAwMDAw
WhcNMjcwMzEyMjM1OTU5WjAyMQswCQYDVQQGEwJVUzEWMBQGA1UEChMNTGV0J3Mg
RW5jcnlwdDELMAkGA1UEAxMCRTYwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAATZ8Z5G
h/ghcWCoJuuj+rnq2h25EqfUJtlRFLFhfHWWvyILOR/VvtEKRqotPEoJhC6+QJVV
6RlAN2Z17TJOdwRJ+HB7wxjnzvdxEP6sdNgA1O1tHHMWMxCcOrLqbGL0vbijgfgw
gfUwDgYDVR0PAQH/BAQDAgGGMB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEFBQcD
ATASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBSTJ0aYA6lRaI6Y1sRCSNsj
v1iU0jAfBgNVHSMEGDAWgBR5tFnme7bl5AFzgAiIyBpY9umbbjAyBggrBgEFBQcB
AQQmMCQwIgYIKwYBBQUHMAKGFmh0dHA6Ly94MS5pLmxlbmNyLm9yZy8wEwYDVR0g
BAwwCjAIBgZngQwBAgEwJwYDVR0fBCAwHjAcoBqgGIYWaHR0cDovL3gxLmMubGVu
Y3Iub3JnLzANBgkqhkiG9w0BAQsFAAOCAgEAfYt7SiA1sgWGCIpunk46r4AExIRc
MxkKgUhNlrrv1B21hOaXN/5miE+LOTbrcmU/M9yvC6MVY730GNFoL8IhJ8j8vrOL
pMY22OP6baS1k9YMrtDTlwJHoGby04ThTUeBDksS9RiuHvicZqBedQdIF65pZuhp
eDcGBcLiYasQr/EO5gxxtLyTmgsHSOVSBcFOn9lgv7LECPq9i7mfH3mpxgrRKSxH
pOoZ0KXMcB+hHuvlklHntvcI0mMMQ0mhYj6qtMFStkF1RpCG3IPdIwpVCQqu8GV7
s8ubknRzs+3C/Bm19RFOoiPpDkwvyNfvmQ14XkyqqKK5oZ8zhD32kFRQkxa8uZSu
h4aTImFxknu39waBxIRXE4jKxlAmQc4QjFZoq1KmQqQg0J/1JF8RlFvJas1VcjLv
YlvUB2t6npO6oQjB3l+PNf0DpQH7iUx3Wz5AjQCi6L25FjyE06q6BZ/QlmtYdl/8
ZYao4SRqPEs/6cAiF+Qf5zg2UkaWtDphl1LKMuTNLotvsX99HP69V2faNyegodQ0
LyTApr/vT01YPE46vNsDLgK+4cL6TrzC/a4WcmF5SRJ938zrv/duJHLXQIku5v0+
EwOy59Hdm0PT/Er/84dDV0CSjdR/2XuZM3kpysSKLgD1cKiDA+IRguODCxfO9cyY
Ig46v9mFmBvyH04=
-----END CERTIFICATE-----
@@ -1,22 +0,0 @@
-----BEGIN CERTIFICATE-----
MIIDkzCCAxmgAwIBAgISBQsXxrfK6EpotI2fXWteV3kMMAoGCCqGSM49BAMDMDIx
CzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MQswCQYDVQQDEwJF
NjAeFw0yNTA3MjgwNTM5MjNaFw0yNTEwMjYwNTM5MjJaMBcxFTATBgNVBAMTDGNs
YW54aWUubGlmZTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABKcExKR+nZ21Hpm6
rqR/QuJ35yRwhBtTim+5cColIDIocjasMhB1ho49IINEa/hp5EHqTsMsgSkGEZ6R
unr3D0qjggIoMIICJDAOBgNVHQ8BAf8EBAMCB4AwHQYDVR0lBBYwFAYIKwYBBQUH
AwEGCCsGAQUFBwMCMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFKlsFBhG/CpyaZvz
3/gFZdP0vxsWMB8GA1UdIwQYMBaAFJMnRpgDqVFojpjWxEJI2yO/WJTSMDIGCCsG
AQUFBwEBBCYwJDAiBggrBgEFBQcwAoYWaHR0cDovL2U2LmkubGVuY3Iub3JnLzAn
BgNVHREEIDAegg4qLmNsYW54aWUubGlmZYIMY2xhbnhpZS5saWZlMBMGA1UdIAQM
MAowCAYGZ4EMAQIBMC0GA1UdHwQmMCQwIqAgoB6GHGh0dHA6Ly9lNi5jLmxlbmNy
Lm9yZy8xMy5jcmwwggECBgorBgEEAdZ5AgQCBIHzBIHwAO4AdQDtPEvW6AbCpKIA
V9vLJOI4Ad9RL+3EhsVwDyDdtz4/4AAAAZhPwIK1AAAEAwBGMEQCIGoi+WQh9Bhb
4f68AFmojUBHdj0cJhH8Wgv26QxqKUXZAiAyrxtJ9viGLjivjDUPm8NjmgquuLeH
UAMkhSdvZmKK1QB1AA3h8jAr0w3BQGISCepVLvxHdHyx1+kw7w5CHrR+Tqo0AAAB
mE/AmiYAAAQDAEYwRAIgbXMFtU65Mrr1ZaQSdX3Jc+5YO1Y/yApwx7vbGKOzriQC
IFgfpun9rGzPSXUBW+oEjkvUeN1Yf2Thu4YIvkUfuceiMAoGCCqGSM49BAMDA2gA
MGUCMCU9wGtvD/A/LqIdeNFtS8/Um7Sv0iyAn8JGcAu/GCI2oavEQk90mPvqMuqX
W2rUQgIxANHH0WTA3121/bK4Rhu5w8kXUI8AWnxizZ4hDjGCKi6WmJCGhLqtleYK
hhrMK8WjmQ==
-----END CERTIFICATE-----
@@ -1,15 +0,0 @@
Le_Domain='clanxie.life'
Le_Alt='*.clanxie.life'
Le_Webroot='dns_cf'
Le_PreHook=''
Le_PostHook=''
Le_RenewHook=''
Le_API='https://acme-v02.api.letsencrypt.org/directory'
Le_Keylength='ec-256'
Le_OrderFinalize='https://acme-v02.api.letsencrypt.org/acme/finalize/2400063957/411715523371'
Le_LinkOrder='https://acme-v02.api.letsencrypt.org/acme/order/2400063957/411715523371'
Le_LinkCert='https://acme-v02.api.letsencrypt.org/acme/cert/050b17c6b7cae84a68b48d9f5d6b5e57790c'
Le_CertCreateTime='1753684679'
Le_CertCreateTimeStr='2025-07-28T06:37:59Z'
Le_NextRenewTimeStr='2025-09-25T06:37:59Z'
Le_NextRenewTime='1758782279'
@@ -1,9 +0,0 @@
-----BEGIN CERTIFICATE REQUEST-----
MIIBKzCB0gIBADAXMRUwEwYDVQQDDAxjbGFueGllLmxpZmUwWTATBgcqhkjOPQIB
BggqhkjOPQMBBwNCAASnBMSkfp2dtR6Zuq6kf0Lid+ckcIQbU4pvuXAqJSAyKHI2
rDIQdYaOPSCDRGv4aeRB6k7DLIEpBhGekbp69w9KoFkwVwYJKoZIhvcNAQkOMUow
SDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwJwYDVR0RBCAwHoIMY2xh
bnhpZS5saWZlgg4qLmNsYW54aWUubGlmZTAKBggqhkjOPQQDAgNIADBFAiEAvyNm
66wN/4Ni5cLfH8KpwHcgoVGXDoVAawiVaMdmsD0CIAKsdhG5SWWqvNYhYjKIVKbh
Fv8cJ8R1rsaQPO98zj5k
-----END CERTIFICATE REQUEST-----
@@ -1,8 +0,0 @@
[ req_distinguished_name ]
[ req ]
distinguished_name = req_distinguished_name
req_extensions = v3_req
[ v3_req ]
extendedKeyUsage=serverAuth,clientAuth
subjectAltName=DNS:clanxie.life,DNS:*.clanxie.life
@@ -1,5 +0,0 @@
-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIJW3lmfzDnyLpgkX0Jlu0J3Bo1OyVa610GHAdvQmah8loAoGCCqGSM49
AwEHoUQDQgAEpwTEpH6dnbUembqupH9C4nfnJHCEG1OKb7lwKiUgMihyNqwyEHWG
jj0gg0Rr+GnkQepOwyyBKQYRnpG6evcPSg==
-----END EC PRIVATE KEY-----
@@ -1,49 +0,0 @@
-----BEGIN CERTIFICATE-----
MIIDkzCCAxmgAwIBAgISBQsXxrfK6EpotI2fXWteV3kMMAoGCCqGSM49BAMDMDIx
CzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MQswCQYDVQQDEwJF
NjAeFw0yNTA3MjgwNTM5MjNaFw0yNTEwMjYwNTM5MjJaMBcxFTATBgNVBAMTDGNs
YW54aWUubGlmZTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABKcExKR+nZ21Hpm6
rqR/QuJ35yRwhBtTim+5cColIDIocjasMhB1ho49IINEa/hp5EHqTsMsgSkGEZ6R
unr3D0qjggIoMIICJDAOBgNVHQ8BAf8EBAMCB4AwHQYDVR0lBBYwFAYIKwYBBQUH
AwEGCCsGAQUFBwMCMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFKlsFBhG/CpyaZvz
3/gFZdP0vxsWMB8GA1UdIwQYMBaAFJMnRpgDqVFojpjWxEJI2yO/WJTSMDIGCCsG
AQUFBwEBBCYwJDAiBggrBgEFBQcwAoYWaHR0cDovL2U2LmkubGVuY3Iub3JnLzAn
BgNVHREEIDAegg4qLmNsYW54aWUubGlmZYIMY2xhbnhpZS5saWZlMBMGA1UdIAQM
MAowCAYGZ4EMAQIBMC0GA1UdHwQmMCQwIqAgoB6GHGh0dHA6Ly9lNi5jLmxlbmNy
Lm9yZy8xMy5jcmwwggECBgorBgEEAdZ5AgQCBIHzBIHwAO4AdQDtPEvW6AbCpKIA
V9vLJOI4Ad9RL+3EhsVwDyDdtz4/4AAAAZhPwIK1AAAEAwBGMEQCIGoi+WQh9Bhb
4f68AFmojUBHdj0cJhH8Wgv26QxqKUXZAiAyrxtJ9viGLjivjDUPm8NjmgquuLeH
UAMkhSdvZmKK1QB1AA3h8jAr0w3BQGISCepVLvxHdHyx1+kw7w5CHrR+Tqo0AAAB
mE/AmiYAAAQDAEYwRAIgbXMFtU65Mrr1ZaQSdX3Jc+5YO1Y/yApwx7vbGKOzriQC
IFgfpun9rGzPSXUBW+oEjkvUeN1Yf2Thu4YIvkUfuceiMAoGCCqGSM49BAMDA2gA
MGUCMCU9wGtvD/A/LqIdeNFtS8/Um7Sv0iyAn8JGcAu/GCI2oavEQk90mPvqMuqX
W2rUQgIxANHH0WTA3121/bK4Rhu5w8kXUI8AWnxizZ4hDjGCKi6WmJCGhLqtleYK
hhrMK8WjmQ==
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIEVzCCAj+gAwIBAgIRALBXPpFzlydw27SHyzpFKzgwDQYJKoZIhvcNAQELBQAw
TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMjQwMzEzMDAwMDAw
WhcNMjcwMzEyMjM1OTU5WjAyMQswCQYDVQQGEwJVUzEWMBQGA1UEChMNTGV0J3Mg
RW5jcnlwdDELMAkGA1UEAxMCRTYwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAATZ8Z5G
h/ghcWCoJuuj+rnq2h25EqfUJtlRFLFhfHWWvyILOR/VvtEKRqotPEoJhC6+QJVV
6RlAN2Z17TJOdwRJ+HB7wxjnzvdxEP6sdNgA1O1tHHMWMxCcOrLqbGL0vbijgfgw
gfUwDgYDVR0PAQH/BAQDAgGGMB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEFBQcD
ATASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBSTJ0aYA6lRaI6Y1sRCSNsj
v1iU0jAfBgNVHSMEGDAWgBR5tFnme7bl5AFzgAiIyBpY9umbbjAyBggrBgEFBQcB
AQQmMCQwIgYIKwYBBQUHMAKGFmh0dHA6Ly94MS5pLmxlbmNyLm9yZy8wEwYDVR0g
BAwwCjAIBgZngQwBAgEwJwYDVR0fBCAwHjAcoBqgGIYWaHR0cDovL3gxLmMubGVu
Y3Iub3JnLzANBgkqhkiG9w0BAQsFAAOCAgEAfYt7SiA1sgWGCIpunk46r4AExIRc
MxkKgUhNlrrv1B21hOaXN/5miE+LOTbrcmU/M9yvC6MVY730GNFoL8IhJ8j8vrOL
pMY22OP6baS1k9YMrtDTlwJHoGby04ThTUeBDksS9RiuHvicZqBedQdIF65pZuhp
eDcGBcLiYasQr/EO5gxxtLyTmgsHSOVSBcFOn9lgv7LECPq9i7mfH3mpxgrRKSxH
pOoZ0KXMcB+hHuvlklHntvcI0mMMQ0mhYj6qtMFStkF1RpCG3IPdIwpVCQqu8GV7
s8ubknRzs+3C/Bm19RFOoiPpDkwvyNfvmQ14XkyqqKK5oZ8zhD32kFRQkxa8uZSu
h4aTImFxknu39waBxIRXE4jKxlAmQc4QjFZoq1KmQqQg0J/1JF8RlFvJas1VcjLv
YlvUB2t6npO6oQjB3l+PNf0DpQH7iUx3Wz5AjQCi6L25FjyE06q6BZ/QlmtYdl/8
ZYao4SRqPEs/6cAiF+Qf5zg2UkaWtDphl1LKMuTNLotvsX99HP69V2faNyegodQ0
LyTApr/vT01YPE46vNsDLgK+4cL6TrzC/a4WcmF5SRJ938zrv/duJHLXQIku5v0+
EwOy59Hdm0PT/Er/84dDV0CSjdR/2XuZM3kpysSKLgD1cKiDA+IRguODCxfO9cyY
Ig46v9mFmBvyH04=
-----END CERTIFICATE-----
+45
View File
@@ -26,6 +26,7 @@ package main
import (
"fmt"
"os"
"strconv"
"github.com/kylelemons/go-gypsy/yaml"
@@ -51,6 +52,17 @@ type Config struct {
WebrtcPort string
WebrtcUsername string
WebrtcPassword string
// LDAP配置 (LDAP Configuration)
LdapEnabled bool
LdapServer string
LdapPort int
LdapUseTLS bool
LdapBindDN string
LdapBindPassword string
LdapBaseDN string
LdapUserFilter string
LdapAllowedGroups string
LdapAllowedUsers string
}
// docker mode fixed path for reading certificate
@@ -89,6 +101,18 @@ func (cfg *Config) Parse(c *cli.Command) error {
getFlagOpt(c, "webrtc-username", &cfg.WebrtcUsername)
getFlagOpt(c, "webrtc-password", &cfg.WebrtcPassword)
// LDAP配置标志 (LDAP Configuration flags)
getFlagOpt(c, "ldap-enabled", &cfg.LdapEnabled)
getFlagOpt(c, "ldap-server", &cfg.LdapServer)
getFlagOpt(c, "ldap-port", &cfg.LdapPort)
getFlagOpt(c, "ldap-use-tls", &cfg.LdapUseTLS)
getFlagOpt(c, "ldap-bind-dn", &cfg.LdapBindDN)
getFlagOpt(c, "ldap-bind-password", &cfg.LdapBindPassword)
getFlagOpt(c, "ldap-base-dn", &cfg.LdapBaseDN)
getFlagOpt(c, "ldap-user-filter", &cfg.LdapUserFilter)
getFlagOpt(c, "ldap-allowed-groups", &cfg.LdapAllowedGroups)
getFlagOpt(c, "ldap-allowed-users", &cfg.LdapAllowedUsers)
return nil
}
@@ -131,6 +155,27 @@ func parseYamlCfg(cfg *Config, conf string) error {
getConfigOpt(yamlCfg, "webrtc-port", &cfg.WebrtcPort)
getConfigOpt(yamlCfg, "webrtc-username", &cfg.WebrtcUsername)
getConfigOpt(yamlCfg, "webrtc-password", &cfg.WebrtcPassword)
// LDAP配置 (LDAP Configuration)
getConfigOpt(yamlCfg, "ldap-enabled", &cfg.LdapEnabled)
getConfigOpt(yamlCfg, "ldap-server", &cfg.LdapServer)
getConfigOpt(yamlCfg, "ldap-port", &cfg.LdapPort)
getConfigOpt(yamlCfg, "ldap-use-tls", &cfg.LdapUseTLS)
getConfigOpt(yamlCfg, "ldap-bind-dn", &cfg.LdapBindDN)
// 注意:为了避免特殊字符解析问题和提高安全性,ldap-bind-password故意不从YAML读取
// Note: ldap-bind-password is intentionally not read from YAML to avoid special character parsing issues and for security.
// It's always read directly from the LDAP_BIND_PASSWORD environment variable below
getConfigOpt(yamlCfg, "ldap-base-dn", &cfg.LdapBaseDN)
getConfigOpt(yamlCfg, "ldap-user-filter", &cfg.LdapUserFilter)
getConfigOpt(yamlCfg, "ldap-allowed-groups", &cfg.LdapAllowedGroups)
getConfigOpt(yamlCfg, "ldap-allowed-users", &cfg.LdapAllowedUsers)
// LDAP密码始终从环境变量读取以避免YAML特殊字符解析问题和提高安全性
// LDAP password is always read from environment variable to avoid YAML special character parsing issues and for security.
if envPassword := os.Getenv("LDAP_BIND_PASSWORD"); envPassword != "" {
cfg.LdapBindPassword = envPassword
}
return nil
}
-29
View File
@@ -1,29 +0,0 @@
version: '3.8'
services:
rttys:
container_name: glkvm_cloud
image: glkvm:v1
ports:
- "443:443"
- "10443:10443"
- "5912:5912"
volumes:
- ./rttys.conf:/home/rttys.conf:ro
- ./certificate/glkvm.cer:/home/certificate/glkvm_cer:ro
- ./certificate/glkvm.key:/home/certificate/glkvm_key:ro
command: ["-c", "/home/rttys.conf"]
restart: always
coturn:
image: coturn/coturn:edge-alpine
container_name: glkvm_coturn
restart: always
ports:
- "3478:3478"
- "3478:3478/udp"
- "5349:5349"
- "5349:5349/udp"
volumes:
- ./turnserver.conf:/etc/turnserver.conf:ro
command: ["-c", "/etc/turnserver.conf"]
+38
View File
@@ -0,0 +1,38 @@
# Images
GLKVM_IMAGE=glzhitong/glkvm-cloud:latest
COTURN_IMAGE=coturn/coturn:edge-alpine
# GLKVM access IP seen by devices/users.
# Leave empty to auto-detect at container start.
GLKVM_ACCESS_IP=
# rttys
RTTYS_TOKEN=DeviceTokenYouCanChangeMe
RTTYS_PASS=StrongP@ssw0rd
RTTYS_DEVICE_PORT=5912
RTTYS_WEBUI_PORT=443
RTTYS_HTTP_PROXY_PORT=10443
# TURN
TURN_PORT=3478
TURN_USER=glkvmcloudwebrtcuser
TURN_PASS=AnotherS3cret
# LDAP Authentication (Optional)
LDAP_ENABLED=false
LDAP_SERVER=your-ldap-server.com
LDAP_PORT=389
LDAP_USE_TLS=false
LDAP_BIND_DN=cn=service-account,ou=users,dc=company,dc=com
LDAP_BIND_PASSWORD=service-password
LDAP_BASE_DN=ou=users,dc=company,dc=com
# User filter examples for different LDAP implementations:
# Active Directory: (&(objectClass=person)(sAMAccountName=%s))
# OpenLDAP: (&(objectClass=inetOrgPerson)(uid=%s))
# FreeIPA: (&(objectClass=person)(uid=%s))
# Generic LDAP: (uid=%s)
LDAP_USER_FILTER=(uid=%s)
LDAP_ALLOWED_GROUPS=admins,operators
LDAP_ALLOWED_USERS=user1,user2
+56
View File
@@ -0,0 +1,56 @@
# Quick Start
This guide shows how to deploy **glkvm-cloud** using the provided Docker Compose environment template.
1. **Clone the repository and prepare the environment template**
```bash
git clone https://github.com/gl-inet/glkvm-cloud.git
cd glkvm-cloud/docker-compose/
cp .env.example .env
```
2. **Configure environment variables**
Edit `.env` and update the required parameters:
- `RTTYS_TOKEN`: device connection token (leave empty to use the default)
- `RTTYS_PASS`: web management password (leave empty to use the default **StrongP@ssw0rd**)
- `TURN_USER` / `TURN_PASS`: coturn authentication credentials (leave empty to use the default)
- `GLKVM_ACCESS_IP`: glkvm cloud access address (leave empty to auto-detect at startup)
**LDAP Authentication (Optional):**
- `LDAP_ENABLED`: set to `true` to enable LDAP authentication (default: `false`)
- `LDAP_SERVER`: LDAP server hostname or IP address
- `LDAP_PORT`: LDAP server port (default: `389`, for TLS use `636`)
- `LDAP_USE_TLS`: set to `true` to enable TLS encryption (default: `false`)
- `LDAP_BIND_DN`: service account distinguished name
- `LDAP_BIND_PASSWORD`: service account password
- `LDAP_BASE_DN`: search base for user queries
- `LDAP_USER_FILTER`: LDAP query filter (default: `(uid=%s)`)
- `LDAP_ALLOWED_GROUPS`: comma-separated list of authorized groups (optional)
- `LDAP_ALLOWED_USERS`: comma-separated list of authorized users (optional)
⚠️ **Note:** All configuration should be done in the `.env` file.
You don’t need to modify `docker-compose.yml`, templates, or scripts directly.
3. **Start the services**
```bash
docker-compose up -d
```
If you modify `.env` or template files, make sure to apply the updates:
```bash
docker-compose down && docker-compose up -d
```
4. **Platform Access**
Once the installation is complete, access the platform via:
```bash
https://<your_server_public_ip>
```
+66
View File
@@ -0,0 +1,66 @@
version: "2.0"
services:
rttys:
image: ${GLKVM_IMAGE:-glzhitong/glkvm-cloud:latest}
container_name: glkvm_cloud
restart: always
environment:
# Preferred: set GLKVM_ACCESS_IP explicitly; if empty, entrypoint will auto-detect once.
GLKVM_ACCESS_IP: ${GLKVM_ACCESS_IP:-}
# ---- rttys ----
RTTYS_TOKEN: ${RTTYS_TOKEN:-DeviceTokenYouCanChangeMe}
RTTYS_PASS: ${RTTYS_PASS:-StrongP@ssw0rd}
# Ports inside container (mirrored to host via `ports` below)
RTTYS_DEVICE_PORT: ${RTTYS_DEVICE_PORT:-5912} # addr-dev
RTTYS_WEBUI_PORT: ${RTTYS_WEBUI_PORT:-443} # addr-user
RTTYS_HTTP_PROXY_PORT: ${RTTYS_HTTP_PROXY_PORT:-10443} # addr-http-proxy
# WebRTC / TURN reference (used by rttys template)
TURN_PORT: ${TURN_PORT:-3478}
TURN_USER: ${TURN_USER:-glkvmcloudwebrtcuser}
TURN_PASS: ${TURN_PASS:-AnotherS3cret}
# ---- LDAP Configuration ----
LDAP_ENABLED: ${LDAP_ENABLED:-false}
LDAP_SERVER: ${LDAP_SERVER:-}
LDAP_PORT: ${LDAP_PORT:-389}
LDAP_USE_TLS: ${LDAP_USE_TLS:-false}
LDAP_BIND_DN: ${LDAP_BIND_DN:-}
LDAP_BIND_PASSWORD: ${LDAP_BIND_PASSWORD:-}
LDAP_BASE_DN: ${LDAP_BASE_DN:-}
LDAP_USER_FILTER: ${LDAP_USER_FILTER:-(uid=%s)}
LDAP_ALLOWED_GROUPS: ${LDAP_ALLOWED_GROUPS:-}
LDAP_ALLOWED_USERS: ${LDAP_ALLOWED_USERS:-}
volumes:
- ./templates/rttys.conf.template:/tpl/rttys.conf.tmpl:ro
- ./scripts/docker-entrypoint.sh:/docker-entrypoint.sh:ro
- ./certificate/glkvm.cer:/home/certificate/glkvm_cer:ro
- ./certificate/glkvm.key:/home/certificate/glkvm_key:ro
entrypoint: ["/bin/sh", "/docker-entrypoint.sh"]
command: ["rttys"]
ports:
- "${RTTYS_WEBUI_PORT:-443}:${RTTYS_WEBUI_PORT:-443}"
- "${RTTYS_HTTP_PROXY_PORT:-10443}:${RTTYS_HTTP_PROXY_PORT:-10443}"
- "${RTTYS_DEVICE_PORT:-5912}:${RTTYS_DEVICE_PORT:-5912}"
coturn:
image: ${COTURN_IMAGE:-coturn/coturn:edge-alpine}
container_name: glkvm_coturn
restart: always
environment:
# Same semantics as above: prefer explicit value, else auto-detect
GLKVM_ACCESS_IP: ${GLKVM_ACCESS_IP:-}
TURN_PORT: ${TURN_PORT:-3478}
TURN_USER: ${TURN_USER:-glkvmcloudwebrtcuser}
TURN_PASS: ${TURN_PASS:-AnotherS3cret}
entrypoint: ["/bin/sh", "/docker-entrypoint.sh"]
command: ["coturn"]
volumes:
- ./templates/turnserver.conf.template:/tpl/turnserver.conf.tmpl:ro
- ./scripts/docker-entrypoint.sh:/docker-entrypoint.sh:ro
ports:
- "${TURN_PORT:-3478}:3478/tcp"
- "${TURN_PORT:-3478}:3478/udp"
+86
View File
@@ -0,0 +1,86 @@
#!/bin/sh
set -e
# -------- GLKVM access IP resolver (IPv4) --------
resolve_glkvm_access_ip() {
# 1) Prefer user-provided env
if [ -n "${GLKVM_ACCESS_IP:-}" ]; then
echo "${GLKVM_ACCESS_IP}"
return 0
fi
is_ipv4() { echo "$1" | grep -Eq '^[0-9]{1,3}(\.[0-9]{1,3}){3}$'; }
try_cmd() {
val="$("$@" 2>/dev/null | tr -d '\r\n')"
if is_ipv4 "$val"; then
echo "$val"; return 0
fi
return 1
}
# 2) Best-effort auto-detection from multiple sources
if command -v wget >/dev/null 2>&1; then
try_cmd wget -qO- --timeout=3 https://api.ipify.org && return 0
try_cmd wget -qO- --timeout=3 https://ifconfig.me && return 0
fi
if command -v curl >/dev/null 2>&1; then
try_cmd curl -fsS --max-time 3 https://api.ipify.org && return 0
try_cmd curl -fsS --max-time 3 https://ifconfig.me && return 0
fi
if command -v dig >/dev/null 2>&1; then
try_cmd sh -c "dig +short -4 myip.opendns.com @resolver1.opendns.com" && return 0
fi
# 3) Fallback
echo "127.0.0.1"
}
# -------- Minimal template renderer ({{KEY}}) --------
render() {
in="$1"; out="$2"; shift 2
cp "$in" "$out"
for key in "$@"; do
val="$(printenv "$key" || true)"
# Special-case: GLKVM_ACCESS_IP prefers env, else auto-detect once
if [ "$key" = "GLKVM_ACCESS_IP" ] && [ -z "$val" ]; then
val="$(resolve_glkvm_access_ip)"
fi
# Escape '/' and '&' for sed
esc=$(printf '%s' "$val" | sed -e 's/[\/&]/\\&/g')
sed -i "s/{{${key}}}/${esc}/g" "$out"
done
}
# -------- Dispatch by first arg --------
case "$1" in
rttys)
: "${RTTYS_DEVICE_PORT:=5912}"
: "${RTTYS_WEBUI_PORT:=443}"
: "${RTTYS_HTTP_PROXY_PORT:=10443}"
: "${TURN_PORT:=3478}"
render /tpl/rttys.conf.tmpl /home/rttys.conf \
RTTYS_TOKEN RTTYS_PASS \
GLKVM_ACCESS_IP TURN_PORT TURN_USER TURN_PASS \
RTTYS_DEVICE_PORT RTTYS_WEBUI_PORT RTTYS_HTTP_PROXY_PORT \
LDAP_ENABLED LDAP_SERVER LDAP_PORT LDAP_USE_TLS \
LDAP_BIND_DN LDAP_BIND_PASSWORD LDAP_BASE_DN \
LDAP_USER_FILTER LDAP_ALLOWED_GROUPS LDAP_ALLOWED_USERS
exec rttys -c /home/rttys.conf
;;
coturn)
: "${TURN_PORT:=3478}"
mkdir -p /tmp
render /tpl/turnserver.conf.tmpl /tmp/turnserver.conf \
GLKVM_ACCESS_IP TURN_PORT TURN_USER TURN_PASS
exec turnserver -c /tmp/turnserver.conf
;;
*)
exec "$@"
;;
esac
+28
View File
@@ -0,0 +1,28 @@
# Authentication token for device connections
token: {{RTTYS_TOKEN}}
# Web management password
password: {{RTTYS_PASS}}
# WebRTC
webrtc-ip: {{GLKVM_ACCESS_IP}}
webrtc-port: {{TURN_PORT}}
webrtc-username: {{TURN_USER}}
webrtc-password: {{TURN_PASS}}
# Listen addresses
addr-dev: :{{RTTYS_DEVICE_PORT}}
addr-user: :{{RTTYS_WEBUI_PORT}}
addr-http-proxy: :{{RTTYS_HTTP_PROXY_PORT}}
# LDAP Authentication
ldap-enabled: {{LDAP_ENABLED}}
ldap-server: {{LDAP_SERVER}}
ldap-port: {{LDAP_PORT}}
ldap-use-tls: {{LDAP_USE_TLS}}
ldap-bind-dn: {{LDAP_BIND_DN}}
# Note: ldap-bind-password is read directly from LDAP_BIND_PASSWORD environment variable
ldap-base-dn: {{LDAP_BASE_DN}}
ldap-user-filter: {{LDAP_USER_FILTER}}
ldap-allowed-groups: {{LDAP_ALLOWED_GROUPS}}
ldap-allowed-users: {{LDAP_ALLOWED_USERS}}
+7
View File
@@ -0,0 +1,7 @@
listening-port={{TURN_PORT}}
lt-cred-mech
user={{TURN_USER}}:{{TURN_PASS}}
realm=glkvm
no-multicast-peers
allowed-peer-ip=0.0.0.0/0
external-ip={{GLKVM_ACCESS_IP}}
+3
View File
@@ -7,6 +7,7 @@ require (
github.com/fanjindong/go-cache v0.0.6
github.com/gin-contrib/cors v1.7.6
github.com/gin-gonic/gin v1.10.1
github.com/go-ldap/ldap/v3 v3.4.8
github.com/google/uuid v1.6.0
github.com/gorilla/websocket v1.5.3
github.com/json-iterator/go v1.1.12
@@ -19,11 +20,13 @@ require (
)
require (
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
github.com/bytedance/sonic v1.13.3 // indirect
github.com/bytedance/sonic/loader v0.2.4 // indirect
github.com/cloudwego/base64x v0.1.5 // indirect
github.com/gabriel-vasile/mimetype v1.4.9 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect
github.com/go-asn1-ber/asn1-ber v1.5.5 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.26.0 // indirect
+73 -10
View File
@@ -1,3 +1,7 @@
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8=
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI=
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
github.com/bytedance/sonic v1.13.3 h1:MS8gmaH16Gtirygw7jV91pDCN33NyMrPbN7qiYhEsF0=
github.com/bytedance/sonic v1.13.3/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
@@ -23,6 +27,10 @@ github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ=
github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
github.com/go-asn1-ber/asn1-ber v1.5.5 h1:MNHlNMBDgEKD4TcKr36vQN68BA00aDfjIt3/bD50WnA=
github.com/go-asn1-ber/asn1-ber v1.5.5/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
github.com/go-ldap/ldap/v3 v3.4.8 h1:loKJyspcRezt2Q3ZRMq2p/0v8iOurlmeXDPw6fikSvQ=
github.com/go-ldap/ldap/v3 v3.4.8/go.mod h1:qS3Sjlu76eHfHGpUdWkAXQTw4beih+cHsco2jXlIXrk=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
@@ -39,8 +47,25 @@ github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8=
github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=
github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo=
github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM=
github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg=
github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo=
github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o=
github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg=
github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8=
github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs=
github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY=
github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
@@ -81,6 +106,7 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
@@ -95,36 +121,73 @@ github.com/urfave/cli/v3 v3.3.8 h1:BzolUExliMdet9NlJ/u4m5vHSotJ3PzEqSAZ1oPMa/E=
github.com/urfave/cli/v3 v3.3.8/go.mod h1:FJSKtM/9AiiTOJL4fJ6TbMUkxBXn7GO9guZqoZtpYpo=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/arch v0.18.0 h1:WN9poc33zL4AzGxqf8VtpKUnGvMi8O9lhNyBMF/85qc=
golang.org/x/arch v0.18.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
golang.org/x/term v0.33.0 h1:NuFncQrRcaRvVmgRkvM3j/F00gWIAlcmlB8ACEKmGIg=
golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0=
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+40 -8
View File
@@ -35,6 +35,7 @@ import (
"net/http"
"net/url"
"strconv"
"strings"
"sync"
"sync/atomic"
"time"
@@ -44,7 +45,6 @@ import (
"github.com/gin-gonic/gin"
"github.com/rs/zerolog/log"
"github.com/valyala/bytebufferpool"
"golang.org/x/net/publicsuffix"
)
type HttpProxySession struct {
@@ -346,13 +346,8 @@ func httpProxyRedirect(srv *RttyServer, c *gin.Context, group string) {
location = fmt.Sprintf("https://%s%s?sid=%s", hostname, cfg.AddrHttpProxy, sid)
log.Info().Msgf("Using IP redirect: %s", location)
} else {
// 域名访问,拼接 devid 子域名
eTLDPlusOne, err := publicsuffix.EffectiveTLDPlusOne(hostname)
if err != nil {
log.Info().Msgf("Error parsing domain: %v", err)
eTLDPlusOne = hostname // fallback
}
location = fmt.Sprintf("https://%s.%s%s?sid=%s", devid, eTLDPlusOne, cfg.AddrHttpProxy, sid)
redirHost := buildRedirectHost(hostname, devid)
location = fmt.Sprintf("https://%s%s?sid=%s", redirHost, cfg.AddrHttpProxy, sid)
log.Info().Msgf("Using domain redirect: %s", location)
}
@@ -624,3 +619,40 @@ func Write302WithCookie(conn net.Conn, location, cookieName, cookieValue string)
)
_, _ = conn.Write([]byte(response))
}
// buildRedirectHost removes the first label of the hostname and prepends devid.
// Rules:
// - "www.example.com" -> "devid.example.com"
// - "www.l1.example.com" -> "devid.l1.example.com"
// - "www.l1.l2.example.com" -> "devid.l1.l2.example.com"
// - Two-level domain "example.com" -> "devid.example.com"
// - Single label / abnormal cases -> "devid." + hostname (fallback)
//
// The input hostname must be a pure hostname without port.
func buildRedirectHost(hostname, devid string) string {
// Allow FQDN with trailing dot like "example.com."
hostname = strings.TrimSuffix(hostname, ".")
// Split into labels
labels := strings.Split(hostname, ".")
// Remove empty labels (in case of consecutive dots)
compact := make([]string, 0, len(labels))
for _, l := range labels {
if l != "" {
compact = append(compact, l)
}
}
labels = compact
switch len(labels) {
case 0:
return devid // extreme case: just return devid
case 1:
// Single label (e.g., "localhost") — keep original as suffix
return devid + "." + labels[0]
default:
// >=2: drop the leftmost label
suffix := strings.Join(labels[1:], ".")
return devid + "." + suffix
}
}
+376
View File
@@ -0,0 +1,376 @@
/*
* @Author: CU-Jon
* @Date: 2025-09-26 13:28:12 EDT
* @LastEditors: CU-Jon
* @LastEditTime: 2025-09-26 14:02:57 EDT
* @FilePath: \glkvm-cloud\ldap.go
* @Description: LDAP认证模块 (LDAP authentication module)
*/
package main
import (
"crypto/tls"
"fmt"
"strings"
"time"
"github.com/go-ldap/ldap/v3"
"github.com/rs/zerolog/log"
)
// LDAP认证器结构体 (LDAP authenticator struct)
type LDAPAuthenticator struct {
config *Config
}
// 创建新的LDAP认证器 (Create new LDAP authenticator)
func NewLDAPAuthenticator(config *Config) *LDAPAuthenticator {
return &LDAPAuthenticator{config: config}
}
// 执行用户LDAP认证 (Perform LDAP authentication for a user)
func (l *LDAPAuthenticator) Authenticate(username, password string) (bool, error) {
if !l.config.LdapEnabled {
return false, fmt.Errorf("LDAP authentication is disabled")
}
if username == "" || password == "" {
return false, fmt.Errorf("username and password are required")
}
// 连接到LDAP服务器 (Connect to LDAP server)
conn, err := l.connect()
if err != nil {
return false, fmt.Errorf("failed to connect to LDAP server: %v", err)
}
defer conn.Close()
// 使用服务账户进行绑定和搜索 (Use service account for binding and searching)
if l.config.LdapBindDN == "" || l.config.LdapBindPassword == "" {
return false, fmt.Errorf("service account credentials are required for LDAP authentication - BindDN empty: %v, BindPassword empty: %v", l.config.LdapBindDN == "", l.config.LdapBindPassword == "")
}
err = conn.Bind(l.config.LdapBindDN, l.config.LdapBindPassword)
if err != nil {
return false, fmt.Errorf("service account bind failed: %v", err)
} // 使用服务账户搜索用户 (Use service account to search for user)
userDN, err := l.findUserDN(conn, username)
if err != nil {
return false, fmt.Errorf("user search failed: %v", err)
}
// 找到用户,现在用用户凭证验证密码 (Found user, now validate password with user credentials)
err = conn.Bind(userDN, password)
if err != nil {
return false, fmt.Errorf("password validation failed: %v", err)
}
// 重新绑定为服务账户以进行授权检查 (Rebind as service account for authorization check)
err = conn.Bind(l.config.LdapBindDN, l.config.LdapBindPassword)
if err != nil {
return false, fmt.Errorf("failed to rebind as service account for authorization: %v", err)
}
// 检查用户授权 (Check user authorization)
authorized, err := l.checkAuthorization(conn, userDN, username)
if err != nil {
return false, fmt.Errorf("authorization check failed: %v", err)
}
if !authorized {
return false, fmt.Errorf("user not authorized")
}
return true, nil
}
// 建立到LDAP服务器的连接 (Establish connection to LDAP server)
func (l *LDAPAuthenticator) connect() (*ldap.Conn, error) {
address := fmt.Sprintf("%s:%d", l.config.LdapServer, l.config.LdapPort)
var conn *ldap.Conn
var err error
if l.config.LdapUseTLS {
// TLS配置 (TLS configuration)
tlsConfig := &tls.Config{
ServerName: l.config.LdapServer,
InsecureSkipVerify: true, // 跳过证书验证以避免自签名证书问题 (Skip certificate verification to avoid self-signed certificate issues)
}
if l.config.LdapPort == 636 {
// 使用LDAPS (直接TLS连接) (Use LDAPS - direct TLS connection)
conn, err = ldap.DialTLS("tcp", address, tlsConfig)
} else {
// 使用StartTLS (先连接再升级到TLS) (Use StartTLS - connect first then upgrade to TLS)
conn, err = ldap.Dial("tcp", address)
if err == nil {
err = conn.StartTLS(tlsConfig)
}
}
} else {
// 使用普通连接 (Use plain connection)
conn, err = ldap.Dial("tcp", address)
}
if err != nil {
return nil, err
}
// 设置超时时间 (Set timeout)
conn.SetTimeout(10 * time.Second)
return conn, nil
}
// 基于用户名搜索用户DN (Search for user DN based on username)
func (l *LDAPAuthenticator) findUserDN(conn *ldap.Conn, username string) (string, error) {
// 准备搜索过滤器 (Prepare search filter)
filter := fmt.Sprintf(l.config.LdapUserFilter, username)
if l.config.LdapUserFilter == "" {
filter = fmt.Sprintf("(uid=%s)", username)
}
// 执行搜索 (Perform search)
searchRequest := ldap.NewSearchRequest(
l.config.LdapBaseDN,
ldap.ScopeWholeSubtree,
ldap.NeverDerefAliases,
0, // 无大小限制 (No size limit)
0, // 无时间限制 (No time limit)
false,
filter,
[]string{"dn"},
nil,
)
sr, err := conn.Search(searchRequest)
if err != nil {
return "", err
}
if len(sr.Entries) == 0 {
return "", fmt.Errorf("user not found")
}
if len(sr.Entries) > 1 {
return "", fmt.Errorf("multiple users found")
}
return sr.Entries[0].DN, nil
}
// 基于组或用户列表检查用户是否授权 (Check if user is authorized based on groups or users list)
func (l *LDAPAuthenticator) checkAuthorization(conn *ldap.Conn, userDN, username string) (bool, error) {
// 如果没有配置限制,则允许所有已认证用户 (If no restrictions are configured, allow all authenticated users)
if l.config.LdapAllowedGroups == "" && l.config.LdapAllowedUsers == "" {
return true, nil
}
// 检查允许的用户列表 (Check allowed users list)
if l.config.LdapAllowedUsers != "" {
allowedUsers := strings.Split(strings.TrimSpace(l.config.LdapAllowedUsers), ",")
for _, allowedUser := range allowedUsers {
if strings.TrimSpace(allowedUser) == username {
return true, nil
}
}
}
// 检查允许的组 (Check allowed groups)
if l.config.LdapAllowedGroups != "" {
return l.checkGroupMembership(conn, userDN, username)
}
return false, nil
}
// 检查用户是否属于任何允许的组 (Check if user belongs to any of the allowed groups)
func (l *LDAPAuthenticator) checkGroupMembership(conn *ldap.Conn, userDN, username string) (bool, error) {
allowedGroups := strings.Split(strings.TrimSpace(l.config.LdapAllowedGroups), ",")
for _, group := range allowedGroups {
group = strings.TrimSpace(group)
if group == "" {
continue
}
// 搜索组成员关系 - 尝试不同的常见LDAP组结构 (Search for group membership - try different common LDAP group structures)
isMember, err := l.isGroupMember(conn, userDN, username, group)
if err != nil {
log.Warn().Msgf("Error checking group membership for %s in %s: %v", username, group, err)
continue
}
if isMember {
return true, nil
}
}
return false, nil
}
// 检查用户是否是指定组的成员 (Check if user is a member of the specified group)
func (l *LDAPAuthenticator) isGroupMember(conn *ldap.Conn, userDN, username, groupName string) (bool, error) {
// 首先查找用户的实际DN,因为我们可能使用了UPN格式进行认证 (First find the user's actual DN, as we may have used UPN format for authentication)
actualUserDN, err := l.findActualUserDN(conn, username)
if err != nil {
actualUserDN = userDN // 回退到原始DN (Fallback to original DN)
}
// 尝试不同的常见组搜索模式 (Try different common group search patterns)
// 模式1:通过CN搜索组并检查成员属性 (Pattern 1: Search for group by CN and check member attribute)
groupFilter := fmt.Sprintf("(cn=%s)", groupName)
groupSearchRequest := ldap.NewSearchRequest(
l.config.LdapBaseDN,
ldap.ScopeWholeSubtree,
ldap.NeverDerefAliases,
0, 0, false,
groupFilter,
[]string{"member", "memberUid", "uniqueMember"},
nil,
)
sr, err := conn.Search(groupSearchRequest)
if err != nil {
log.Warn().Msgf("Group search failed: %v", err)
return false, err
}
for _, entry := range sr.Entries {
members := entry.GetAttributeValues("member")
memberUids := entry.GetAttributeValues("memberUid")
uniqueMembers := entry.GetAttributeValues("uniqueMember")
// 检查member属性(完整DN) (Check member attribute - full DN)
for _, member := range members {
if member == userDN || member == actualUserDN {
return true, nil
}
// 也检查是否member DN包含用户名 (Also check if member DN contains the username)
if strings.Contains(strings.ToLower(member), strings.ToLower("cn="+username)) {
return true, nil
}
}
// 检查memberUid属性(仅用户名) (Check memberUid attribute - username only)
for _, memberUid := range memberUids {
if memberUid == username {
return true, nil
}
}
// 检查uniqueMember属性(完整DN) (Check uniqueMember attribute - full DN)
for _, uniqueMember := range uniqueMembers {
if uniqueMember == userDN {
return true, nil
}
}
}
// 模式2:通过用户名搜索用户并检查memberOf属性 (Pattern 2: Search for user by username and check memberOf attribute)
// 使用配置的用户过滤器或默认的uid过滤器 (Use configured user filter or default uid filter)
userFilter := fmt.Sprintf(l.config.LdapUserFilter, username)
if l.config.LdapUserFilter == "" {
userFilter = fmt.Sprintf("(uid=%s)", username)
}
userSearchRequest := ldap.NewSearchRequest(
l.config.LdapBaseDN,
ldap.ScopeWholeSubtree,
ldap.NeverDerefAliases,
0, 0, false,
userFilter,
[]string{"memberOf", "distinguishedName"},
nil,
)
sr, err = conn.Search(userSearchRequest)
if err != nil {
log.Warn().Msgf("User search for memberOf failed: %v", err)
} else {
for _, entry := range sr.Entries {
memberOfValues := entry.GetAttributeValues("memberOf")
for _, memberOf := range memberOfValues {
if strings.Contains(strings.ToLower(memberOf), strings.ToLower("cn="+groupName)) {
return true, nil
}
}
}
}
return false, nil
}
// 查找用户的实际DN (Find the user's actual DN)
func (l *LDAPAuthenticator) findActualUserDN(conn *ldap.Conn, username string) (string, error) {
// 使用配置的用户过滤器搜索用户 (Search for user using configured user filter)
userFilter := fmt.Sprintf(l.config.LdapUserFilter, username)
if l.config.LdapUserFilter == "" {
userFilter = fmt.Sprintf("(uid=%s)", username)
}
userSearchRequest := ldap.NewSearchRequest(
l.config.LdapBaseDN,
ldap.ScopeWholeSubtree,
ldap.NeverDerefAliases,
0, 0, false,
userFilter,
[]string{"distinguishedName"},
nil,
)
sr, err := conn.Search(userSearchRequest)
if err != nil {
return "", err
}
if len(sr.Entries) == 0 {
return "", fmt.Errorf("user not found")
}
if len(sr.Entries) > 1 {
return "", fmt.Errorf("multiple users found")
}
return sr.Entries[0].DN, nil
}
// 执行用户认证,支持LDAP和传统密码认证 (Perform user authentication with LDAP and legacy password support)
func AuthenticateUser(cfg *Config, username, password, authMethod string) bool {
success, _ := AuthenticateUserWithError(cfg, username, password, authMethod)
return success
}
// 执行用户认证并返回错误类型,支持LDAP和传统密码认证 (Perform user authentication with error type, supporting LDAP and legacy password authentication)
func AuthenticateUserWithError(cfg *Config, username, password, authMethod string) (bool, string) {
// 处理LDAP认证 (Handle LDAP authentication)
if cfg.LdapEnabled && authMethod == "ldap" && username != "" {
ldapAuth := NewLDAPAuthenticator(cfg)
success, err := ldapAuth.Authenticate(username, password)
if err != nil {
log.Error().Msgf("LDAP authentication error: %v", err)
// 检查错误类型以区分认证和授权错误 (Check error type to distinguish between authentication and authorization errors)
if strings.Contains(err.Error(), "user not authorized") {
return false, "authorization"
}
return false, "authentication"
}
return success, ""
}
// 回退到原始密码认证以保持向后兼容 (Fallback to original password authentication for backward compatibility)
if authMethod == "legacy" || authMethod == "" {
if cfg.Password == password {
return true, ""
}
return false, "authentication"
}
return false, "authentication"
}
+42
View File
@@ -119,6 +119,48 @@ func main() {
Name: "allow-origins",
Usage: "allow all origins for cross-domain request",
},
&cli.BoolFlag{
Name: "ldap-enabled",
Usage: "enable LDAP authentication",
},
&cli.StringFlag{
Name: "ldap-server",
Usage: "LDAP server hostname or IP",
},
&cli.IntFlag{
Name: "ldap-port",
Value: 389,
Usage: "LDAP server port",
},
&cli.BoolFlag{
Name: "ldap-use-tls",
Usage: "use TLS/SSL for LDAP connection",
},
&cli.StringFlag{
Name: "ldap-bind-dn",
Usage: "LDAP bind DN for service account",
},
&cli.StringFlag{
Name: "ldap-bind-password",
Usage: "LDAP bind password for service account",
},
&cli.StringFlag{
Name: "ldap-base-dn",
Usage: "LDAP base DN for user searches",
},
&cli.StringFlag{
Name: "ldap-user-filter",
Value: "(uid=%s)",
Usage: "LDAP user filter",
},
&cli.StringFlag{
Name: "ldap-allowed-groups",
Usage: "comma-separated list of allowed LDAP groups",
},
&cli.StringFlag{
Name: "ldap-allowed-users",
Usage: "comma-separated list of allowed LDAP users",
},
&cli.StringFlag{
Name: "pprof",
Usage: "enable pprof and listen on specified address (e.g. localhost:6060)",
Vendored
+1 -1
View File
@@ -8,7 +8,7 @@
*/
/// <reference types="vite/client" />
import type { BaseButton, BaseText } from '@gl/main/components'
import type { BaseButton, BaseText } from 'gl-web-main/components'
import type BaseSvg from './src/components/base/baseSvg.vue'
declare module 'vue' {
+1 -1
View File
@@ -18,7 +18,6 @@
"prepare": "husky"
},
"dependencies": {
"@gl/main": "0.0.50",
"@vue/eslint-config-typescript": "12.0.0",
"@xterm/addon-fit": "0.10.0",
"@xterm/addon-web-links": "0.11.0",
@@ -26,6 +25,7 @@
"ant-design-vue": "^4.2.6",
"axios": "^1.9.0",
"dayjs": "^1.11.13",
"gl-web-main": "^1.0.0",
"js-cookie": "^3.0.5",
"jsencrypt": "^3.3.2",
"pinia": "^3.0.2",
+2 -2
View File
@@ -27,8 +27,8 @@ import { computed } from 'vue'
import { useAppStore } from './stores/modules/app'
import { RouterView } from 'vue-router'
import type { ThemeConfig } from 'ant-design-vue/es/config-provider/context'
import { ConfigProvider as GlConfigProvider } from '@gl/main/components'
import { Languages } from '@gl/main'
import { ConfigProvider as GlConfigProvider } from 'gl-web-main/components'
import { Languages } from 'gl-web-main'
const appStore = useAppStore()
+4 -4
View File
@@ -1,15 +1,15 @@
/*
* @Author: LPY
* @Date: 2025-06-03 09:29:03
* @LastEditors: LPY
* @LastEditTime: 2025-08-25 19:23:29
* @LastEditors: CU-Jon
* @LastEditTime: 2025-09-27 03:16:38 EDT
* @FilePath: \glkvm-cloud\web-ui\src\api\request.ts
* @Description: 请求统一配置文件
*/
import { NotNeedHandledRequestErrorCodeList, RequestErrorCodeEnum } from '@/models/request'
import { useUserStore } from '@/stores/modules/user'
import { showErrorMessage } from './requestError'
import { BaseResponse, HttpService } from '@gl/main'
import { BaseResponse, HttpService } from 'gl-web-main'
import type { AxiosResponse } from 'axios'
export const httpService = new HttpService(
@@ -43,7 +43,7 @@ export const httpService = new HttpService(
},
error => {
console.log('请求错误', error)
Promise.reject(error)
return Promise.reject(error)
},
)
+10 -3
View File
@@ -1,14 +1,14 @@
/*
* @Author: LPY
* @Date: 2025-06-03 12:21:21
* @LastEditors: LPY
* @LastEditTime: 2025-08-26 09:06:31
* @LastEditors: CU-Jon
* @LastEditTime: 2025-09-26 14:02:57 EDT
* @FilePath: \glkvm-cloud\web-ui\src\api\user.ts
* @Description: 用户相关请求api
*/
import request from './request'
import type { LoginParams } from '@/models/user'
import type { LoginParams, AuthConfig } from '@/models/user'
/** 登录 */
export function reqLogin (data: LoginParams) {
@@ -31,4 +31,11 @@ export function reqCheckLoginStatus () {
return request<void>({
url: '/alive',
})
}
/** 获取认证配置 */
export function reqAuthConfig () {
return request<AuthConfig>({
url: '/auth-config',
})
}
@@ -19,8 +19,8 @@
</template>
<script setup lang="ts" generic="T">
import type { SelectOptions } from '@gl/main'
import { BaseDivider, BaseDropdownSelect } from '@gl/main/components'
import type { SelectOptions } from 'gl-web-main'
import { BaseDivider, BaseDropdownSelect } from 'gl-web-main/components'
import type { Trigger } from 'ant-design-vue/es/dropdown/props'
import { computed } from 'vue'
+2 -2
View File
@@ -36,8 +36,8 @@
<script setup lang="ts" generic="T extends AnyObject">
import type { DropdownGroupItem } from '@/models/component'
import type { AnyObject } from '@gl/main'
import { BaseDivider } from '@gl/main/components'
import type { AnyObject } from 'gl-web-main'
import { BaseDivider } from 'gl-web-main/components'
import { Dropdown, Menu, Radio, RadioGroup } from 'ant-design-vue'
import type { Trigger } from 'ant-design-vue/es/dropdown/props'
import { computed, reactive, watch } from 'vue'
+1 -1
View File
@@ -46,7 +46,7 @@
</template>
<script setup lang="ts">
import { debounce } from '@gl/main'
import { debounce } from 'gl-web-main'
import { ref } from 'vue'
const props = withDefaults(defineProps<{
+1 -1
View File
@@ -24,7 +24,7 @@
<script setup lang="ts" generic="T extends BaseData" >
import type { BaseTableProps } from '@/models/component'
import { isEmpty, type BaseData } from '@gl/main'
import { isEmpty, type BaseData } from 'gl-web-main'
import { Table } from 'ant-design-vue'
import { useSlots } from 'vue'
+1 -1
View File
@@ -24,7 +24,7 @@
</template>
<script setup lang="ts">
import { ViewType } from '@gl/main'
import { ViewType } from 'gl-web-main'
import { Tooltip } from 'ant-design-vue'
const props = withDefaults(defineProps<{
+2 -2
View File
@@ -53,11 +53,11 @@
<script setup lang="ts">
import useLanguage from '@/hooks/useLanguage'
import { languageOptions, Languages } from '@gl/main'
import { languageOptions, Languages } from 'gl-web-main'
import BaseStep from './baseStep.vue'
import { useRoute } from 'vue-router'
import { useUserStore } from '@/stores/modules/user'
import { BaseDropdownSelect } from '@gl/main/components'
import { BaseDropdownSelect } from 'gl-web-main/components'
import { isForeignEnv } from '@/utils'
const route = useRoute()
+1 -1
View File
@@ -6,7 +6,7 @@
* @FilePath: \kvm-cloud-frontend\src\hooks\useDeviceQueue.ts
* @Description: 自动发现设备队列
*/
import { AnyObject, deepClone, isEmpty } from '@gl/main'
import { AnyObject, deepClone, isEmpty } from 'gl-web-main'
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
const DEFAULT_QUEUE_INTERVAL = 400
+1 -1
View File
@@ -11,7 +11,7 @@ import i18n from '@/lang'
import { languageLabelMap } from '@/models/setting'
import { computed } from 'vue'
import { LocalStorageKeys, useLocalStorage } from './useLocalStorage'
import { Languages } from '@gl/main'
import { Languages } from 'gl-web-main'
/** 语言hook */
export default function useLanguage () {
// @ts-ignore
+1 -1
View File
@@ -6,7 +6,7 @@
* @FilePath: \gl-cloud-frontend\src\hooks\usePageLink.js
* @Description: 用于处理分页相关的逻辑
*/
import type { AnyObject, TableDataResponse } from '@gl/main'
import type { AnyObject, TableDataResponse } from 'gl-web-main'
import { computed, ref } from 'vue'
export const GLOBAL_PAGE_SIZE = 'GLOBAL_PAGE_SIZE'
+1 -1
View File
@@ -8,7 +8,7 @@
*/
import { computed } from 'vue'
import useLanguage from './useLanguage'
import type { SelectOptions } from '@gl/main'
import type { SelectOptions } from 'gl-web-main'
export const useTranslatedOptions = <T>(options: SelectOptions<T>[]) => {
const { t } = useLanguage()
+1 -1
View File
@@ -10,7 +10,7 @@ import { createI18n } from 'vue-i18n'
import zh from './locales/zh.json'
import en from './locales/en.json'
import useLanguage from '@/hooks/useLanguage'
import { Languages } from '@gl/main'
import { Languages } from 'gl-web-main'
const i18n = createI18n({
legacy: false,
+6 -1
View File
@@ -18,11 +18,16 @@
},
"login": {
"authorizationRequired": "Authorization Required",
"username": "Username",
"password": "Password",
"signIn": "Sign In",
"enterPwdTip": "Please enter your password",
"incorrectPwd": "Incorrect Password",
"signOut": "Sign Out"
"notAuthorized": "Not Authorized",
"signOut": "Sign Out",
"authOptions": "Authentication Options",
"ldapAuth": "Enter username and password for LDAP authentication",
"webManagementAuth": "Leave username empty and use web management password"
},
"device": {
"devices": "Devices",
+6 -1
View File
@@ -18,11 +18,16 @@
},
"login": {
"authorizationRequired": "需要授权",
"username": "用户名",
"password": "密码",
"signIn": "登录",
"enterPwdTip": "请输入密码",
"incorrectPwd": "密码错误",
"signOut": "退出"
"notAuthorized": "未授权",
"signOut": "退出",
"authOptions": "认证方式",
"ldapAuth": "输入用户名和密码进行LDAP认证",
"webManagementAuth": "留空用户名并使用Web管理密码"
},
"device": {
"devices": "设备数",
+1 -1
View File
@@ -8,7 +8,7 @@
*/
import { createApp } from 'vue'
import '@gl/main/style.css'
import 'gl-web-main/style.css'
import '@/styles/index.scss'
import App from './App.vue'
import projectInitialize from './projectInitialize'
+1 -1
View File
@@ -6,7 +6,7 @@
* @FilePath: \kvm-cloud-frontend\src\models\component.ts
* @Description: 组件有关的models
*/
import type { SelectOptions } from '@gl/main'
import type { SelectOptions } from 'gl-web-main'
import type { TableProps } from 'ant-design-vue'
export interface DropdownGroupItem<T = any> {
+1 -1
View File
@@ -7,7 +7,7 @@
* @Description: 设置相关类型声明
*/
import { Languages, SelectOptions } from '@gl/main'
import { Languages, SelectOptions } from 'gl-web-main'
/** 语言对应的label映射 */
export const languageLabelMap = new Map<Languages, string>([
+11 -3
View File
@@ -1,13 +1,21 @@
/*
* @Author: LPY
* @Date: 2025-06-09 16:37:00
* @LastEditors: LPY
* @LastEditTime: 2025-08-22 11:19:48
* @LastEditors: CU-Jon
* @LastEditTime: 2025-09-26 14:02:57 EDT
* @FilePath: \glkvm-cloud\web-ui\src\models\user.ts
* @Description: 用户相关类型声明
*/
/** 登录参数 */
/** 登录参数 (Login parameters) */
export interface LoginParams {
username?: string;
password: string;
authMethod?: 'ldap' | 'legacy';
}
/** 认证配置 (Authentication configuration) */
export interface AuthConfig {
ldapEnabled: boolean;
legacyPassword: boolean;
}
+1 -1
View File
@@ -10,7 +10,7 @@ import type { App } from 'vue'
import '@/assets/iconfont/iconfont.js'
import BaseSvg from '@/components/base/baseSvg.vue'
import { BaseButton, BaseText } from '@gl/main/components'
import { BaseButton, BaseText } from 'gl-web-main/components'
/** 全局注册自定义组件 */
const installComponent = function (app: App) {
+1 -1
View File
@@ -7,7 +7,7 @@
* @Description: app相关状态存储
*/
import { LocalStorageKeys, useLocalStorage } from '@/hooks/useLocalStorage'
import { baseTheme, createStyleInsert, darkTheme, replaceAntTheme, ThemeMode } from '@gl/main'
import { baseTheme, createStyleInsert, darkTheme, replaceAntTheme, ThemeMode } from 'gl-web-main'
import type { ThemeConfig } from 'ant-design-vue/es/config-provider/context'
import { defineStore } from 'pinia'
import { computed, reactive } from 'vue'
+1 -1
View File
@@ -8,7 +8,7 @@
*/
import { getDeviceListApi } from '@/api/device'
import { type DeviceInfo, type DeviceQuery } from '@/models/device'
import { PageLink } from '@gl/main'
import { PageLink } from 'gl-web-main'
import { defineStore } from 'pinia'
import { computed, reactive, ref, watch } from 'vue'
+15 -4
View File
@@ -1,8 +1,8 @@
/*
* @Author: LPY
* @Date: 2025-05-29 18:43:46
* @LastEditors: LPY
* @LastEditTime: 2025-08-26 09:24:09
* @LastEditors: CU-Jon
* @LastEditTime: 2025-09-26 14:02:57 EDT
* @FilePath: \glkvm-cloud\web-ui\src\stores\modules\user.ts
* @Description: 用户相关状态存储
*/
@@ -37,10 +37,19 @@ export const useUserStore = defineStore('user', () => {
/** 登录 */
const login = async (credentials: LoginParams) => {
// 加密密码
const params = {
// 准备登录参数 (Prepare login parameters)
const params: LoginParams = {
password: credentials.password,
}
// 如果提供了用户名和认证方法则添加 (Add username and auth method if provided)
if (credentials.username) {
params.username = credentials.username
}
if (credentials.authMethod) {
params.authMethod = credentials.authMethod
}
const data = await reqLogin(params)
console.log(data.info)
loginStatus.value = true
@@ -74,6 +83,8 @@ export const useUserStore = defineStore('user', () => {
logout()
} catch (error) {
console.log(error)
// 即使退出请求失败也继续本地退出 (Continue local logout even if logout request fails)
logout()
}
}
+1 -1
View File
@@ -8,7 +8,7 @@
*/
import { t } from '@/hooks/useLanguage'
import { glConfirm, type BaseConfirmProps } from '@gl/main'
import { glConfirm, type BaseConfirmProps } from 'gl-web-main'
import { message } from 'ant-design-vue'
import { ValidateErrorEntity } from 'ant-design-vue/es/form/interface'
import { AxiosError } from 'axios'
@@ -31,9 +31,9 @@
<script setup lang="ts">
import { reactive, watch } from 'vue'
import { BaseModal } from '@gl/main/components'
import { BaseModal } from 'gl-web-main/components'
import { getAddDeviceScriptInfoApi } from '@/api/device'
import { copyText } from '@gl/main'
import { copyText } from 'gl-web-main'
import { message } from 'ant-design-vue'
import { t } from '@/hooks/useLanguage'
@@ -26,7 +26,7 @@
</template>
<script setup lang="ts">
import { BaseModal } from '@gl/main/components'
import { BaseModal } from 'gl-web-main/components'
const props = defineProps<{ open: boolean, type: string, res: any }>()
@@ -79,8 +79,8 @@ import { reqExecuteCommand } from '@/api/device'
import BaseTable from '@/components/base/baseTable.vue'
import { t } from '@/hooks/useLanguage'
import { DeviceInfo, ExecuteCommandFormData } from '@/models/device'
import { useFakeUpgradeProgress } from '@gl/main'
import { BaseModal } from '@gl/main/components'
import { useFakeUpgradeProgress } from 'gl-web-main'
import { BaseModal } from 'gl-web-main/components'
import { TableColumnType } from 'ant-design-vue'
import { computed, reactive, watch } from 'vue'
import CmdResDetailDialog from './cmdResDetailDialog.vue'
@@ -64,8 +64,8 @@
<script setup lang="ts">
import { reactive, ref, watch } from 'vue'
import { BaseModal } from '@gl/main/components'
import { FormRules, OnBeforeOk, useValidateInfo } from '@gl/main'
import { BaseModal } from 'gl-web-main/components'
import { FormRules, OnBeforeOk, useValidateInfo } from 'gl-web-main'
import { t } from '@/hooks/useLanguage'
import { DeviceInfo, ExecuteCommandFormData } from '@/models/device'
import { FormInstance } from 'ant-design-vue'
+1 -1
View File
@@ -44,7 +44,7 @@ import '@xterm/xterm/css/xterm.css'
import OverlayAddon from './components/xterm-addon-overlay'
import ContextMenu from './components/contextMenu.vue'
import RttyKeyboard from './components/rttyKeyboard.vue'
import { BaseModal } from '@gl/main/components'
import { BaseModal } from 'gl-web-main/components'
import { message, Modal } from 'ant-design-vue'
import { t } from '@/hooks/useLanguage'
+1 -1
View File
@@ -47,7 +47,7 @@ import NoDevicePage from './components/noDevicePage.vue'
import { useDeviceStore } from '@/stores/modules/device'
import DeviceListView from './components/deviceListView.vue'
import { reactive, onBeforeUnmount, onMounted } from 'vue'
import { BaseLoading } from '@gl/main/components'
import { BaseLoading } from 'gl-web-main/components'
import AddDeviceDialog from './components/addDeviceDialog.vue'
const deviceStore = useDeviceStore()
+157 -13
View File
@@ -1,17 +1,45 @@
<!--
* @Author: LPY
* @Date: 2025-05-30 10:48:43
* @LastEditors: LPY
* @LastEditTime: 2025-08-26 17:11:12
* @LastEditors: CU-Jon
* @LastEditTime: 2025-09-26 14:02:57 EDT
* @FilePath: \glkvm-cloud\web-ui\src\views\login\loginPage.vue
* @Description: 登录页面
-->
<template>
<BaseWhitePage>
<LoginBox>
<BaseText type="large-title-m" style="margin: 24px 0 16px;">
{{ $t('login.authorizationRequired') }}
</BaseText>
<div style="display: flex; align-items: center; justify-content: center; margin: 24px 0 16px;">
<BaseText type="large-title-m">
{{ $t('login.authorizationRequired') }}
</BaseText>
<!-- 认证选项帮助 - 仅在启用LDAP时显示 (Auth options help - only show when LDAP is enabled) -->
<div v-if="isLdapEnabled" class="auth-help-container">
<div
class="help-icon"
@mouseenter="showTooltip = true"
@mouseleave="showTooltip = false"
>
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="currentColor"
style="margin-left: 8px; color: #656d76; cursor: help;"
>
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"/>
<path d="M5.255 5.786a.237.237 0 0 0 .241.247h.825c.138 0 .248-.113.266-.25.09-.656.54-1.134 1.342-1.134.686 0 1.314.343 1.314 1.168 0 .635-.374.927-.965 1.371-.673.489-1.206 1.06-1.168 1.987l.003.217a.25.25 0 0 0 .25.246h.811a.25.25 0 0 0 .25-.25v-.105c0-.718.273-.927 1.01-1.486.609-.463 1.244-.977 1.244-2.056 0-1.511-1.276-2.241-2.673-2.241-1.267 0-2.655.59-2.75 2.286zm1.557 5.763c0 .533.425.927 1.01.927.609 0 1.028-.394 1.028-.927 0-.552-.42-.94-1.029-.94-.584 0-1.009.388-1.009.40z"/>
</svg>
</div>
<!-- 提示框 (Tooltip) -->
<div v-show="showTooltip" class="auth-tooltip">
<div style="font-weight: bold; margin-bottom: 4px;">{{ $t('login.authOptions') }}:</div>
<div>• {{ $t('login.ldapAuth') }}</div>
<div>• {{ $t('login.webManagementAuth') }}</div>
</div>
</div>
</div>
<AForm
class="dense-form"
ref="formRef"
@@ -21,6 +49,16 @@
style="width: 100%;"
@validate="handleValidate"
>
<!-- 用户名字段 - 仅在启用LDAP时显示 (Username field - only show if LDAP is enabled) -->
<AFormItem v-if="isLdapEnabled" name="username">
<AInput
name="username"
v-model:value="state.formModel.username"
:placeholder="$t('login.username')"
@pressEnter="handleLogin"
/>
</AFormItem>
<AFormItem name="password">
<GlPassword
name="password"
@@ -41,32 +79,65 @@
<script setup lang="ts">
import BaseWhitePage from '@/components/base/baseWhitePage.vue'
import LoginBox from './components/loginBox.vue'
import { computed, reactive, ref } from 'vue'
import { computed, reactive, ref, onMounted } from 'vue'
import { t } from '@/hooks/useLanguage'
import { useUserStore } from '@/stores/modules/user'
import { useValidateInfo, type FormRules } from '@gl/main'
import { GlPassword } from '@gl/main/components'
import { useValidateInfo, type FormRules } from 'gl-web-main'
import { GlPassword } from 'gl-web-main/components'
import { useRouter } from 'vue-router'
import { LoginParams } from '@/models/user'
import { message } from 'ant-design-vue'
import { LoginParams, AuthConfig } from '@/models/user'
import { message, Input, Form } from 'ant-design-vue'
import { reqAuthConfig } from '@/api/user'
const AInput = Input
const AForm = Form
const AFormItem = Form.Item
const router = useRouter()
const { handleValidate } = useValidateInfo<LoginParams>()
const formRef = ref(null)
const authConfig = ref<AuthConfig | null>(null)
// 计算属性来可靠地检查LDAP是否启用 (Computed property to reliably check if LDAP is enabled)
const isLdapEnabled = computed(() => {
return authConfig.value?.ldapEnabled === true
})
const state = reactive<{formModel: LoginParams, loading: boolean}>({
formModel: {
username: '',
password: '',
},
loading: false,
})
const showTooltip = ref(false)
const formRules = computed<FormRules<LoginParams>>(() => {
return {
const rules: FormRules<LoginParams> = {
password: [{ required: true, message: 'login.enterPwdTip'}],
}
// 用户名为可选字段,支持双重认证模式 (Username is optional, supporting dual authentication modes)
return rules
})
// 加载认证配置 (Load authentication configuration)
onMounted(async () => {
try {
const response = await reqAuthConfig()
// 提取配置数据 (Extract config data)
const configData = response?.info || response?.data?.info || response?.data || response
authConfig.value = configData
} catch (error) {
console.error('Failed to load auth config:', error)
// 回退 - 无LDAP可用 (Fallback - no LDAP available)
authConfig.value = { ldapEnabled: false, legacyPassword: true }
}
})
// 登录按钮
@@ -74,7 +145,14 @@ const handleLogin = () => {
formRef.value.validate().then(async () => {
state.loading = true
try {
await useUserStore().login(state.formModel)
// 基于用户名自动确定认证方法 (Auto-determine auth method based on username)
const loginData: LoginParams = {
username: state.formModel.username,
password: state.formModel.password,
authMethod: (state.formModel.username && authConfig.value?.ldapEnabled) ? 'ldap' : 'legacy'
}
await useUserStore().login(loginData)
// 登录成功后跳转到首页或之前尝试访问的页面
const redirect = router.currentRoute.value.query.redirect as string || '/'
console.log(redirect)
@@ -84,11 +162,77 @@ const handleLogin = () => {
} catch (error) {
console.log(error)
state.loading = false
message.error(t('login.incorrectPwd'))
// 检查后端返回的错误类型 (Check error type returned by backend)
const errorData = error?.response?.data
const backendError = errorData?.error || ''
// 根据后端返回的具体错误类型显示不同消息 (Show different messages based on specific error type from backend)
if (backendError === 'user not authorized') {
// 授权失败 - 用户存在但权限不足 (Authorization failure - user exists but insufficient permissions)
message.error(t('login.notAuthorized'))
} else {
// 认证失败 - 用户名/密码错误 (Authentication failure - incorrect username/password)
message.error(t('login.incorrectPwd'))
}
}
})
}
</script>
<style scoped lang="scss">
.auth-help-container {
position: relative;
display: inline-block;
}
.help-icon {
display: inline-flex;
align-items: center;
transition: color 0.2s ease;
&:hover {
color: #0066cc !important;
}
}
.auth-tooltip {
position: absolute;
top: 100%;
left: 50%;
transform: translateX(-50%);
margin-top: 8px;
padding: 12px;
background-color: #2c3e50;
color: white;
border-radius: 6px;
font-size: 14px;
width: 320px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
z-index: 1000;
// 添加箭头 (Add arrow)
&::before {
content: '';
position: absolute;
top: -6px;
left: 50%;
transform: translateX(-50%);
width: 0;
height: 0;
border-left: 6px solid transparent;
border-right: 6px solid transparent;
border-bottom: 6px solid #2c3e50;
}
// 确保多行文本正确显示 (Ensure multi-line text displays correctly)
div {
white-space: normal;
line-height: 1.4;
&:not(:last-child) {
margin-bottom: 4px;
}
}
}
</style>
+8 -8
View File
@@ -23,35 +23,35 @@ export default defineConfig(({ mode }) => {
port: 3011,
proxy: {
'/devs': {
target: 'https://49.7.174.146:1443',
target: 'https://106.55.158.199',
secure: false,
},
'/signin': {
target: 'https://49.7.174.146:1443/',
target: 'https://106.55.158.199',
secure: false,
},
'/signout': {
target: 'https://49.7.174.146:1443',
target: 'https://106.55.158.199',
secure: false,
},
'/alive': {
target: 'https://49.7.174.146:1443',
target: 'https://106.55.158.199',
secure: false,
},
'/get': {
target: 'https://49.7.174.146:1443',
target: 'https://106.55.158.199',
secure: false,
},
'^/cmd/.*': {
target: 'https://49.7.174.146:1443',
target: 'https://106.55.158.199',
secure: false,
},
'^/connect/.*': {
ws: true,
target: 'https://49.7.174.146:1443',
target: 'https://106.55.158.199',
},
'^/web/*': {
target: 'https://49.7.174.146:1443',
target: 'https://106.55.158.199',
},
},
},
+553 -529
View File
File diff suppressed because it is too large Load Diff