mirror of
https://github.com/gl-inet/glkvm-cloud.git
synced 2026-10-04 12:41:42 +00:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 72f1986309 | |||
| e9e773408d | |||
| f6f06582dd | |||
| 1ab54dae68 | |||
| d9a66add94 | |||
| de48a9041e | |||
| a31607ad09 | |||
| a9711565cd |
@@ -15,7 +15,7 @@ Self-Deployed Lightweight Cloud is a lightweight KVM remote cloud platform tailo
|
||||
- **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
|
||||
- **Enterprise Authentication** - Supports both **LDAP** and **OIDC** login methods for enterprise users.
|
||||
|
||||
## Self-Hosting Guide
|
||||
|
||||
@@ -182,6 +182,30 @@ For detailed LDAP configuration options and setup instructions, see the [Docker
|
||||
- **LDAP Authentication**: Enter username and password for directory service authentication
|
||||
- **Legacy Authentication**: Leave username empty and use the web management password
|
||||
|
||||
#### 🔐 OIDC Authentication Configuration (Optional)
|
||||
|
||||
GLKVM Cloud provides full support for **OIDC (OpenID Connect)** authentication, allowing seamless integration with modern identity providers such as **Google, Auth0, Authing** and any other standard-compliant OIDC provider.
|
||||
|
||||
**Key Features**
|
||||
|
||||
- **Modern Authentication**
|
||||
Secure sign-in through any OIDC provider supporting Authorization Code Flow.
|
||||
- **Email / Username / Group Whitelisting**
|
||||
Restrict access based on:
|
||||
- Email or domain (e.g. *@example.com*)
|
||||
- Stable user ID (*sub*)
|
||||
- Username (*preferred_username* or *name*)
|
||||
- Groups attribute
|
||||
- **Full OpenID Connect Compliance**
|
||||
Supports issuer validation, token signature verification, and nonce protection.
|
||||
- **Flexible Provider Support**
|
||||
Works with public clouds (Google, Azure AD, Auth0, Okta) and self-hosted solutions.
|
||||
|
||||
**Configuration**
|
||||
|
||||
For detailed OIDC configuration options and setup instructions, see the
|
||||
**[Docker Compose README](docker-compose/README.md)**.
|
||||
|
||||
#### 🔄 Restart Services After Configuration Changes
|
||||
|
||||
After replacing certificates or updating LDAP configuration, restart the GLKVM Cloud services to apply the changes:
|
||||
|
||||
+21
-1
@@ -16,7 +16,7 @@
|
||||
* **数据安全** - 私有化部署,数据完全可控
|
||||
* **独享带宽** - 自部署环境下可享受专属带宽
|
||||
* **轻量设计** - 专为小型企业和个人优化
|
||||
* **企业级认证** - 支持 LDAP 登录,适用于企业用户
|
||||
* **企业级认证** - 同时支持 **LDAP** 和 **OIDC** 登录方式,适用于企业用户。
|
||||
|
||||
## 自部署指南
|
||||
|
||||
@@ -155,6 +155,7 @@ Web UI 的默认登录密码会在安装脚本运行结束时显示:
|
||||
GLKVM 轻量云支持 LDAP 身份认证,适用于企业环境,可以与现有的目录服务(如 Active Directory、OpenLDAP 或 FreeIPA)集成。
|
||||
|
||||
**主要功能:**
|
||||
|
||||
- **双重认证模式**:同时支持 LDAP 和传统密码认证
|
||||
- **基于组的授权**:限制特定 LDAP 组访问
|
||||
- **基于用户的授权**:仅允许特定用户访问
|
||||
@@ -168,6 +169,25 @@ GLKVM 轻量云支持 LDAP 身份认证,适用于企业环境,可以与现
|
||||
- **LDAP 认证**:输入用户名和密码进行目录服务认证
|
||||
- **传统认证**:留空用户名并使用 Web 管理密码
|
||||
|
||||
#### 🔐 OIDC 登录认证配置(可选)
|
||||
|
||||
GLKVM Cloud 完整支持 **OIDC(OpenID Connect)** 登录认证,可无缝集成现代身份提供商,例如 **Google、Auth0、Authing**,以及任何符合 OIDC 标准的认证服务。
|
||||
|
||||
**主要功能**
|
||||
|
||||
- **现代化认证方式**
|
||||
支持使用任意支持 Authorization Code Flow 的 OIDC 身份提供商进行安全登录。
|
||||
- **邮箱 / 用户名 / 用户组白名单控制**
|
||||
可根据以下信息限制用户访问:
|
||||
- 邮箱或域名(如 *@example.com*)
|
||||
- 用户 ID(*sub*)
|
||||
- 用户名(*preferred_username* 或 *name*)
|
||||
- 用户组
|
||||
- **完全符合 OpenID Connect 标准**
|
||||
支持 Issuer 校验、ID Token 签名验证、Nonce 防重放保护等安全机制。
|
||||
- **高度灵活的提供商支持**
|
||||
兼容各类公共云 IdP(Google、Azure AD、Auth0、Okta 等)以及自建身份服务(Keycloak、Authentik、Dex 等)。
|
||||
|
||||
#### 🔄 配置更改后重启服务
|
||||
|
||||
替换证书或更新 LDAP 配置后,需要重启 GLKVM 轻量云服务以应用更改:
|
||||
|
||||
@@ -215,65 +215,68 @@ func (srv *RttyServer) ListenAPI() error {
|
||||
c.Status(http.StatusOK)
|
||||
})
|
||||
|
||||
r.POST("/signin", func(c *gin.Context) {
|
||||
type credentials struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
AuthMethod string `json:"authMethod"`
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
// 自动确定认证方法或使用指定的方法 (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"
|
||||
}
|
||||
}
|
||||
// 自动确定认证方法或使用指定的方法 (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"
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
// 根据错误类型返回适当的错误信息 (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"})
|
||||
}
|
||||
})
|
||||
// 根据错误类型返回适当的错误信息 (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"})
|
||||
}
|
||||
})
|
||||
|
||||
r.GET("/auth-config", func(c *gin.Context) {
|
||||
authConfig := gin.H{
|
||||
"ldapEnabled": cfg.LdapEnabled,
|
||||
"legacyPassword": cfg.Password != "",
|
||||
}
|
||||
c.JSON(http.StatusOK, authConfig)
|
||||
})
|
||||
r.GET("/auth-config", func(c *gin.Context) {
|
||||
authConfig := gin.H{
|
||||
"ldapEnabled": cfg.LdapEnabled,
|
||||
"legacyPassword": cfg.Password != "",
|
||||
"oidcEnabled": cfg.OIDCEnabled,
|
||||
}
|
||||
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)
|
||||
}
|
||||
})
|
||||
|
||||
// ===== 添加OIDC路由 =====
|
||||
RegisterOIDCRoutes(r, cfg)
|
||||
fs, err := fs.Sub(staticFs, "ui/dist")
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -25,171 +25,271 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/kylelemons/go-gypsy/yaml"
|
||||
"github.com/urfave/cli/v3"
|
||||
"github.com/kylelemons/go-gypsy/yaml"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
AddrDev string
|
||||
AddrUser string
|
||||
AddrHttpProxy string
|
||||
HttpProxyRedirURL string
|
||||
HttpProxyRedirDomain string
|
||||
Token string
|
||||
DevHookUrl string
|
||||
UserHookUrl string
|
||||
LocalAuth bool
|
||||
Password string
|
||||
AllowOrigins bool
|
||||
PprofAddr string
|
||||
SslCert string
|
||||
SslKey string
|
||||
WebrtcIP string
|
||||
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
|
||||
AddrDev string
|
||||
AddrUser string
|
||||
AddrHttpProxy string
|
||||
HttpProxyRedirURL string
|
||||
HttpProxyRedirDomain string
|
||||
Token string
|
||||
DevHookUrl string
|
||||
UserHookUrl string
|
||||
LocalAuth bool
|
||||
Password string
|
||||
AllowOrigins bool
|
||||
PprofAddr string
|
||||
SslCert string
|
||||
SslKey string
|
||||
WebrtcIP string
|
||||
WebrtcPort string
|
||||
WebrtcUsername string
|
||||
WebrtcPassword string
|
||||
// // LDAP Configuration
|
||||
LdapEnabled bool
|
||||
LdapServer string
|
||||
LdapPort int
|
||||
LdapUseTLS bool
|
||||
LdapBindDN string
|
||||
LdapBindPassword string
|
||||
LdapBaseDN string
|
||||
LdapUserFilter string
|
||||
LdapAllowedGroups string
|
||||
LdapAllowedUsers string
|
||||
|
||||
// Generic OIDC Provider (supports any standard OIDC provider)
|
||||
OIDCEnabled bool
|
||||
OIDCGenericIssuer string
|
||||
OIDCGenericClientID string
|
||||
OIDCGenericClientSecret string
|
||||
OIDCGenericAuthURL string
|
||||
OIDCGenericTokenURL string
|
||||
OIDCGenericRedirectURL string
|
||||
OIDCGenericScopes []string
|
||||
OIDCGenericAllowedUsers []string
|
||||
OIDCGenericAllowedSubs []string
|
||||
OIDCGenericAllowedUsernames []string
|
||||
OIDCGenericAllowedGroups []string
|
||||
}
|
||||
|
||||
// docker mode fixed path for reading certificate
|
||||
const (
|
||||
SslCert = "/home/certificate/glkvm_cer"
|
||||
SslKey = "/home/certificate/glkvm_key"
|
||||
SslCert = "/home/certificate/glkvm_cer"
|
||||
SslKey = "/home/certificate/glkvm_key"
|
||||
)
|
||||
|
||||
func (cfg *Config) Parse(c *cli.Command) error {
|
||||
conf := c.String("conf")
|
||||
if conf != "" {
|
||||
err := parseYamlCfg(cfg, conf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
conf := c.String("conf")
|
||||
if conf != "" {
|
||||
err := parseYamlCfg(cfg, conf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
getFlagOpt(c, "addr-dev", &cfg.AddrDev)
|
||||
getFlagOpt(c, "addr-user", &cfg.AddrUser)
|
||||
getFlagOpt(c, "addr-http-proxy", &cfg.AddrHttpProxy)
|
||||
getFlagOpt(c, "http-proxy-redir-url", &cfg.HttpProxyRedirURL)
|
||||
getFlagOpt(c, "http-proxy-redir-domain", &cfg.HttpProxyRedirDomain)
|
||||
getFlagOpt(c, "dev-hook-url", &cfg.DevHookUrl)
|
||||
getFlagOpt(c, "user-hook-url", &cfg.UserHookUrl)
|
||||
getFlagOpt(c, "local-auth", &cfg.LocalAuth)
|
||||
getFlagOpt(c, "token", &cfg.Token)
|
||||
getFlagOpt(c, "password", &cfg.Password)
|
||||
getFlagOpt(c, "allow-origins", &cfg.AllowOrigins)
|
||||
getFlagOpt(c, "pprof", &cfg.PprofAddr)
|
||||
getFlagOpt(c, "addr-dev", &cfg.AddrDev)
|
||||
getFlagOpt(c, "addr-user", &cfg.AddrUser)
|
||||
getFlagOpt(c, "addr-http-proxy", &cfg.AddrHttpProxy)
|
||||
getFlagOpt(c, "http-proxy-redir-url", &cfg.HttpProxyRedirURL)
|
||||
getFlagOpt(c, "http-proxy-redir-domain", &cfg.HttpProxyRedirDomain)
|
||||
getFlagOpt(c, "dev-hook-url", &cfg.DevHookUrl)
|
||||
getFlagOpt(c, "user-hook-url", &cfg.UserHookUrl)
|
||||
getFlagOpt(c, "local-auth", &cfg.LocalAuth)
|
||||
getFlagOpt(c, "token", &cfg.Token)
|
||||
getFlagOpt(c, "password", &cfg.Password)
|
||||
getFlagOpt(c, "allow-origins", &cfg.AllowOrigins)
|
||||
getFlagOpt(c, "pprof", &cfg.PprofAddr)
|
||||
|
||||
cfg.SslCert = SslCert
|
||||
cfg.SslKey = SslKey
|
||||
cfg.SslCert = SslCert
|
||||
cfg.SslKey = SslKey
|
||||
|
||||
getFlagOpt(c, "webrtc-ip", &cfg.WebrtcIP)
|
||||
getFlagOpt(c, "webrtc-port", &cfg.WebrtcPort)
|
||||
getFlagOpt(c, "webrtc-username", &cfg.WebrtcUsername)
|
||||
getFlagOpt(c, "webrtc-password", &cfg.WebrtcPassword)
|
||||
getFlagOpt(c, "webrtc-ip", &cfg.WebrtcIP)
|
||||
getFlagOpt(c, "webrtc-port", &cfg.WebrtcPort)
|
||||
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)
|
||||
// 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
|
||||
// Generic OIDC Provider (standard OIDC provider flags)
|
||||
getFlagOpt(c, "oidc-enabled", &cfg.OIDCEnabled)
|
||||
getFlagOpt(c, "oidc-generic-issuer", &cfg.OIDCGenericIssuer)
|
||||
getFlagOpt(c, "oidc-generic-client-id", &cfg.OIDCGenericClientID)
|
||||
getFlagOpt(c, "oidc-generic-client-secret", &cfg.OIDCGenericClientSecret)
|
||||
getFlagOpt(c, "oidc-generic-auth-url", &cfg.OIDCGenericAuthURL)
|
||||
getFlagOpt(c, "oidc-generic-token-url", &cfg.OIDCGenericTokenURL)
|
||||
getFlagOpt(c, "oidc-generic-redirect-url", &cfg.OIDCGenericRedirectURL)
|
||||
getFlagOpt(c, "oidc-generic-scopes", &cfg.OIDCGenericScopes)
|
||||
getFlagOpt(c, "oidc-generic-allowed-users", &cfg.OIDCGenericAllowedUsers)
|
||||
getFlagOpt(c, "oidc-generic-allowed-subs", &cfg.OIDCGenericAllowedSubs)
|
||||
getFlagOpt(c, "oidc-generic-allowed-usernames", &cfg.OIDCGenericAllowedUsernames)
|
||||
getFlagOpt(c, "oidc-generic-allowed-groups", &cfg.OIDCGenericAllowedGroups)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getConfigOpt(yamlCfg *yaml.File, name string, opt any) {
|
||||
val, err := yamlCfg.Get(name)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
val, err := yamlCfg.Get(name)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
switch opt := opt.(type) {
|
||||
case *string:
|
||||
*opt = val
|
||||
case *int:
|
||||
*opt, _ = strconv.Atoi(val)
|
||||
case *bool:
|
||||
*opt, _ = strconv.ParseBool(val)
|
||||
}
|
||||
switch opt := opt.(type) {
|
||||
case *string:
|
||||
*opt = val
|
||||
case *int:
|
||||
*opt, _ = strconv.Atoi(val)
|
||||
case *bool:
|
||||
*opt, _ = strconv.ParseBool(val)
|
||||
}
|
||||
}
|
||||
|
||||
func parseYamlCfg(cfg *Config, conf string) error {
|
||||
yamlCfg, err := yaml.ReadFile(conf)
|
||||
if err != nil {
|
||||
return fmt.Errorf(`read config file: %s`, err.Error())
|
||||
}
|
||||
yamlCfg, err := yaml.ReadFile(conf)
|
||||
if err != nil {
|
||||
return fmt.Errorf(`read config file: %s`, err.Error())
|
||||
}
|
||||
|
||||
getConfigOpt(yamlCfg, "addr-dev", &cfg.AddrDev)
|
||||
getConfigOpt(yamlCfg, "addr-user", &cfg.AddrUser)
|
||||
getConfigOpt(yamlCfg, "addr-http-proxy", &cfg.AddrHttpProxy)
|
||||
getConfigOpt(yamlCfg, "http-proxy-redir-url", &cfg.HttpProxyRedirURL)
|
||||
getConfigOpt(yamlCfg, "http-proxy-redir-domain", &cfg.HttpProxyRedirDomain)
|
||||
getConfigOpt(yamlCfg, "addr-dev", &cfg.AddrDev)
|
||||
getConfigOpt(yamlCfg, "addr-user", &cfg.AddrUser)
|
||||
getConfigOpt(yamlCfg, "addr-http-proxy", &cfg.AddrHttpProxy)
|
||||
getConfigOpt(yamlCfg, "http-proxy-redir-url", &cfg.HttpProxyRedirURL)
|
||||
getConfigOpt(yamlCfg, "http-proxy-redir-domain", &cfg.HttpProxyRedirDomain)
|
||||
|
||||
getConfigOpt(yamlCfg, "token", &cfg.Token)
|
||||
getConfigOpt(yamlCfg, "dev-hook-url", &cfg.DevHookUrl)
|
||||
getConfigOpt(yamlCfg, "user-hook-url", &cfg.UserHookUrl)
|
||||
getConfigOpt(yamlCfg, "local-auth", &cfg.LocalAuth)
|
||||
getConfigOpt(yamlCfg, "password", &cfg.Password)
|
||||
getConfigOpt(yamlCfg, "allow-origins", &cfg.AllowOrigins)
|
||||
getConfigOpt(yamlCfg, "token", &cfg.Token)
|
||||
getConfigOpt(yamlCfg, "dev-hook-url", &cfg.DevHookUrl)
|
||||
getConfigOpt(yamlCfg, "user-hook-url", &cfg.UserHookUrl)
|
||||
getConfigOpt(yamlCfg, "local-auth", &cfg.LocalAuth)
|
||||
getConfigOpt(yamlCfg, "password", &cfg.Password)
|
||||
getConfigOpt(yamlCfg, "allow-origins", &cfg.AllowOrigins)
|
||||
|
||||
getConfigOpt(yamlCfg, "webrtc-ip", &cfg.WebrtcIP)
|
||||
getConfigOpt(yamlCfg, "webrtc-port", &cfg.WebrtcPort)
|
||||
getConfigOpt(yamlCfg, "webrtc-username", &cfg.WebrtcUsername)
|
||||
getConfigOpt(yamlCfg, "webrtc-password", &cfg.WebrtcPassword)
|
||||
getConfigOpt(yamlCfg, "webrtc-ip", &cfg.WebrtcIP)
|
||||
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
|
||||
// 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)
|
||||
|
||||
// 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 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
|
||||
}
|
||||
|
||||
// ===== OIDC configuration (generic OIDC provider) =====
|
||||
// Switch and basic endpoints
|
||||
getConfigOpt(yamlCfg, "oidc-enabled", &cfg.OIDCEnabled)
|
||||
getConfigOpt(yamlCfg, "oidc-generic-issuer", &cfg.OIDCGenericIssuer)
|
||||
getConfigOpt(yamlCfg, "oidc-generic-client-id", &cfg.OIDCGenericClientID)
|
||||
|
||||
// Note: oidc-generic-client-secret is intentionally not read from YAML
|
||||
// to avoid checking secrets into config files and leaking in logs.
|
||||
// It is always read directly from the OIDC_CLIENT_SECRET environment variable below.
|
||||
if envSecret := os.Getenv("OIDC_CLIENT_SECRET"); envSecret != "" {
|
||||
cfg.OIDCGenericClientSecret = envSecret
|
||||
}
|
||||
|
||||
getConfigOpt(yamlCfg, "oidc-generic-auth-url", &cfg.OIDCGenericAuthURL)
|
||||
getConfigOpt(yamlCfg, "oidc-generic-token-url", &cfg.OIDCGenericTokenURL)
|
||||
getConfigOpt(yamlCfg, "oidc-generic-redirect-url", &cfg.OIDCGenericRedirectURL)
|
||||
|
||||
// OIDC scopes (string can be space- or comma-separated, parsed by splitScopes)
|
||||
if s, err := yamlCfg.Get("oidc-generic-scopes"); err == nil && strings.TrimSpace(s) != "" {
|
||||
cfg.OIDCGenericScopes = splitScopes(s)
|
||||
}
|
||||
// Default scopes (when OIDC is enabled but scopes are still empty)
|
||||
if cfg.OIDCEnabled && len(cfg.OIDCGenericScopes) == 0 {
|
||||
cfg.OIDCGenericScopes = []string{"openid", "profile", "email"}
|
||||
}
|
||||
|
||||
// Whitelists for OIDC logins, all parsed via splitScopes (space/comma/newline separated)
|
||||
// 1) Email-based whitelist
|
||||
if s, err := yamlCfg.Get("oidc-generic-allowed-users"); err == nil && strings.TrimSpace(s) != "" {
|
||||
cfg.OIDCGenericAllowedUsers = splitScopes(s)
|
||||
}
|
||||
|
||||
// 2) Subject (sub) whitelist
|
||||
if s, err := yamlCfg.Get("oidc-generic-allowed-subs"); err == nil && strings.TrimSpace(s) != "" {
|
||||
cfg.OIDCGenericAllowedSubs = splitScopes(s)
|
||||
}
|
||||
|
||||
// 3) Username whitelist (preferred_username / name)
|
||||
if s, err := yamlCfg.Get("oidc-generic-allowed-usernames"); err == nil && strings.TrimSpace(s) != "" {
|
||||
cfg.OIDCGenericAllowedUsernames = splitScopes(s)
|
||||
}
|
||||
|
||||
// 4) Groups whitelist
|
||||
if s, err := yamlCfg.Get("oidc-generic-allowed-groups"); err == nil && strings.TrimSpace(s) != "" {
|
||||
cfg.OIDCGenericAllowedGroups = splitScopes(s)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getFlagOpt(c *cli.Command, name string, opt any) {
|
||||
if !c.IsSet(name) {
|
||||
return
|
||||
}
|
||||
if !c.IsSet(name) {
|
||||
return
|
||||
}
|
||||
|
||||
switch opt := opt.(type) {
|
||||
case *string:
|
||||
*opt = c.String(name)
|
||||
case *int:
|
||||
*opt = c.Int(name)
|
||||
case *bool:
|
||||
*opt = c.Bool(name)
|
||||
}
|
||||
switch opt := opt.(type) {
|
||||
case *string:
|
||||
*opt = c.String(name)
|
||||
case *int:
|
||||
*opt = c.Int(name)
|
||||
case *bool:
|
||||
*opt = c.Bool(name)
|
||||
}
|
||||
}
|
||||
|
||||
// splitScopes splits a whitespace/comma/newline-separated scope string
|
||||
// into a deduplicated, cleaned string slice.
|
||||
func splitScopes(s string) []string {
|
||||
// Replace commas with spaces to unify delimiters
|
||||
s = strings.ReplaceAll(s, ",", " ")
|
||||
// Remove optional YAML list characters such as brackets (lenient parsing)
|
||||
s = strings.NewReplacer("[", " ", "]", " ").Replace(s)
|
||||
|
||||
parts := strings.Fields(s)
|
||||
uniq := make([]string, 0, len(parts))
|
||||
seen := make(map[string]struct{}, len(parts))
|
||||
for _, p := range parts {
|
||||
p = strings.TrimSpace(p)
|
||||
if p == "" || p == "-" { // Support accidental "-" items
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[p]; ok {
|
||||
continue
|
||||
}
|
||||
seen[p] = struct{}{}
|
||||
uniq = append(uniq, p)
|
||||
}
|
||||
return uniq
|
||||
}
|
||||
|
||||
@@ -36,3 +36,28 @@ LDAP_USER_FILTER=(uid=%s)
|
||||
|
||||
LDAP_ALLOWED_GROUPS=admins,operators
|
||||
LDAP_ALLOWED_USERS=user1,user2
|
||||
|
||||
# OIDC Authentication (Optional, generic OIDC provider)
|
||||
OIDC_ENABLED=false
|
||||
OIDC_ISSUER=
|
||||
OIDC_CLIENT_ID=
|
||||
OIDC_CLIENT_SECRET=
|
||||
OIDC_AUTH_URL=
|
||||
OIDC_TOKEN_URL=
|
||||
|
||||
# Redirect URL registered in your OIDC provider.
|
||||
# The path part (/auth/oidc/callback) is fixed by GLKVM Cloud and must not be changed.
|
||||
# Example:
|
||||
# OIDC_REDIRECT_URL=https://your-domain.example.com/auth/oidc/callback
|
||||
OIDC_REDIRECT_URL=
|
||||
|
||||
OIDC_SCOPES="openid profile email"
|
||||
|
||||
# Email-based whitelist (exact email or domain like @example.com)
|
||||
OIDC_ALLOWED_USERS=
|
||||
# Subject (sub) whitelist (stable user IDs)
|
||||
OIDC_ALLOWED_SUBS=
|
||||
# Username whitelist (preferred_username or name)
|
||||
OIDC_ALLOWED_USERNAMES=
|
||||
# Groups whitelist (e.g. admin, devops)
|
||||
OIDC_ALLOWED_GROUPS=
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
- `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`)
|
||||
@@ -35,6 +35,29 @@
|
||||
⚠️ **Note:** All configuration should be done in the `.env` file.
|
||||
You don’t need to modify `docker-compose.yml`, templates, or scripts directly.
|
||||
|
||||
**OIDC Authentication (Optional):**
|
||||
|
||||
- `OIDC_ENABLED`: set to `true` to enable OIDC authentication (default: `false`)
|
||||
- `OIDC_ISSUER`: OIDC issuer URL provided by your identity provider
|
||||
(e.g. `https://accounts.google.com`, `https://your-tenant.auth0.com/`)
|
||||
- `OIDC_CLIENT_ID`: client ID issued by your OIDC provider
|
||||
- `OIDC_CLIENT_SECRET`: client secret issued by your OIDC provider
|
||||
*(recommended to set via environment variable rather than YAML files)*
|
||||
- `OIDC_AUTH_URL`: authorization endpoint URL
|
||||
- `OIDC_TOKEN_URL`: token endpoint URL
|
||||
- `OIDC_REDIRECT_URL`: redirect (callback) URL registered in your OIDC provider
|
||||
Domain is user-defined, but the path must be fixed: `/auth/oidc/callback`
|
||||
Example: `https://your-domain.example.com/auth/oidc/callback`
|
||||
- `OIDC_SCOPES`: space-separated list of requested scopes (default: `"openid profile email"`)
|
||||
- `OIDC_ALLOWED_USERS`: comma-separated list of allowed emails or domains (optional)
|
||||
Example: `user@example.com,@example.com`
|
||||
- `OIDC_ALLOWED_SUBS`: comma-separated list of allowed OIDC subject (`sub`) IDs (optional)
|
||||
- `OIDC_ALLOWED_USERNAMES`: comma-separated list of allowed usernames (`preferred_username` or `name`) (optional)
|
||||
- `OIDC_ALLOWED_GROUPS`: comma-separated list of allowed OIDC groups (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
|
||||
|
||||
@@ -34,6 +34,21 @@ services:
|
||||
LDAP_USER_FILTER: ${LDAP_USER_FILTER:-(uid=%s)}
|
||||
LDAP_ALLOWED_GROUPS: ${LDAP_ALLOWED_GROUPS:-}
|
||||
LDAP_ALLOWED_USERS: ${LDAP_ALLOWED_USERS:-}
|
||||
|
||||
# ---- OIDC Authentication ----
|
||||
OIDC_ENABLED: ${OIDC_ENABLED:-false}
|
||||
OIDC_ISSUER: ${OIDC_ISSUER:-}
|
||||
OIDC_CLIENT_ID: ${OIDC_CLIENT_ID:-}
|
||||
OIDC_CLIENT_SECRET: ${OIDC_CLIENT_SECRET:-}
|
||||
OIDC_AUTH_URL: ${OIDC_AUTH_URL:-}
|
||||
OIDC_TOKEN_URL: ${OIDC_TOKEN_URL:-}
|
||||
OIDC_REDIRECT_URL: ${OIDC_REDIRECT_URL:-}
|
||||
OIDC_SCOPES: ${OIDC_SCOPES:-openid profile email}
|
||||
# Whitelists (optional)
|
||||
OIDC_ALLOWED_USERS: ${OIDC_ALLOWED_USERS:-}
|
||||
OIDC_ALLOWED_SUBS: ${OIDC_ALLOWED_SUBS:-}
|
||||
OIDC_ALLOWED_USERNAMES: ${OIDC_ALLOWED_USERNAMES:-}
|
||||
OIDC_ALLOWED_GROUPS: ${OIDC_ALLOWED_GROUPS:-}
|
||||
volumes:
|
||||
- ./templates/rttys.conf.template:/tpl/rttys.conf.tmpl:ro
|
||||
- ./scripts/docker-entrypoint.sh:/docker-entrypoint.sh:ro
|
||||
|
||||
@@ -65,7 +65,10 @@ case "$1" in
|
||||
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
|
||||
LDAP_USER_FILTER LDAP_ALLOWED_GROUPS LDAP_ALLOWED_USERS \
|
||||
OIDC_ENABLED OIDC_CLIENT_ID OIDC_AUTH_URL OIDC_TOKEN_URL \
|
||||
OIDC_REDIRECT_URL OIDC_CLIENT_SECRET OIDC_SCOPES OIDC_ALLOWED_USERS OIDC_ISSUER \
|
||||
OIDC_ALLOWED_SUBS OIDC_ALLOWED_USERNAMES OIDC_ALLOWED_GROUPS
|
||||
|
||||
exec rttys -c /home/rttys.conf
|
||||
;;
|
||||
|
||||
@@ -26,3 +26,24 @@ ldap-base-dn: {{LDAP_BASE_DN}}
|
||||
ldap-user-filter: {{LDAP_USER_FILTER}}
|
||||
ldap-allowed-groups: {{LDAP_ALLOWED_GROUPS}}
|
||||
ldap-allowed-users: {{LDAP_ALLOWED_USERS}}
|
||||
|
||||
# OIDC Authentication (generic OIDC provider)
|
||||
oidc-enabled: {{OIDC_ENABLED}}
|
||||
|
||||
oidc-generic-issuer: {{OIDC_ISSUER}}
|
||||
oidc-generic-client-id: {{OIDC_CLIENT_ID}}
|
||||
|
||||
# Note: oidc-generic-client-secret is read directly from the OIDC_CLIENT_SECRET
|
||||
# environment variable and is intentionally not parsed from YAML to avoid
|
||||
# leaking secrets in config files.
|
||||
|
||||
oidc-generic-auth-url: {{OIDC_AUTH_URL}}
|
||||
oidc-generic-token-url: {{OIDC_TOKEN_URL}}
|
||||
oidc-generic-redirect-url: {{OIDC_REDIRECT_URL}}
|
||||
oidc-generic-scopes: {{OIDC_SCOPES}}
|
||||
|
||||
# Whitelists (all are optional)
|
||||
oidc-generic-allowed-users: {{OIDC_ALLOWED_USERS}}
|
||||
oidc-generic-allowed-subs: {{OIDC_ALLOWED_SUBS}}
|
||||
oidc-generic-allowed-usernames: {{OIDC_ALLOWED_USERNAMES}}
|
||||
oidc-generic-allowed-groups: {{OIDC_ALLOWED_GROUPS}}
|
||||
|
||||
@@ -3,12 +3,14 @@ module rttys
|
||||
go 1.24.4
|
||||
|
||||
require (
|
||||
github.com/coreos/go-oidc/v3 v3.16.0
|
||||
github.com/dwdcth/consoleEx v0.0.0-20180521133551-f56f6eb78b76
|
||||
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/sessions v1.2.1
|
||||
github.com/gorilla/websocket v1.5.3
|
||||
github.com/json-iterator/go v1.1.12
|
||||
github.com/kylelemons/go-gypsy v1.0.0
|
||||
@@ -27,10 +29,12 @@ require (
|
||||
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-jose/go-jose/v4 v4.1.3 // 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
|
||||
github.com/goccy/go-json v0.10.5 // indirect
|
||||
github.com/gorilla/securecookie v1.1.1 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
|
||||
github.com/kr/text v0.2.0 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
@@ -43,6 +47,7 @@ require (
|
||||
golang.org/x/arch v0.18.0 // indirect
|
||||
golang.org/x/crypto v0.40.0 // indirect
|
||||
golang.org/x/net v0.42.0 // indirect
|
||||
golang.org/x/oauth2 v0.28.0 // indirect
|
||||
golang.org/x/sys v0.34.0 // indirect
|
||||
golang.org/x/text v0.27.0 // indirect
|
||||
google.golang.org/protobuf v1.36.6 // indirect
|
||||
|
||||
@@ -10,6 +10,8 @@ github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFos
|
||||
github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
|
||||
github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
||||
github.com/coreos/go-oidc/v3 v3.16.0 h1:qRQUCFstKpXwmEjDQTIbyY/5jF00+asXzSkmkoa/mow=
|
||||
github.com/coreos/go-oidc/v3 v3.16.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
@@ -29,6 +31,8 @@ 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-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
|
||||
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
||||
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=
|
||||
@@ -47,7 +51,9 @@ 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 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
|
||||
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
|
||||
github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI=
|
||||
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=
|
||||
@@ -144,6 +150,8 @@ 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/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc=
|
||||
golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
|
||||
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=
|
||||
|
||||
@@ -25,222 +25,279 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
_ "net/http/pprof"
|
||||
"os"
|
||||
"runtime"
|
||||
"runtime/debug"
|
||||
"context"
|
||||
"encoding/json"
|
||||
_ "net/http/pprof"
|
||||
"os"
|
||||
"runtime"
|
||||
"runtime/debug"
|
||||
|
||||
xlog "rttys/log"
|
||||
xlog "rttys/log"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/urfave/cli/v3"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
const RttysVersion = "5.2.0"
|
||||
|
||||
var (
|
||||
GitCommit = ""
|
||||
BuildTime = ""
|
||||
GitCommit = ""
|
||||
BuildTime = ""
|
||||
)
|
||||
|
||||
func main() {
|
||||
defaultLogPath := "/var/log/rttys.log"
|
||||
if runtime.GOOS == "windows" {
|
||||
defaultLogPath = "rttys.log"
|
||||
}
|
||||
defaultLogPath := "/var/log/rttys.log"
|
||||
if runtime.GOOS == "windows" {
|
||||
defaultLogPath = "rttys.log"
|
||||
}
|
||||
|
||||
cmd := &cli.Command{
|
||||
Name: "rttys",
|
||||
Usage: "The server side for rtty",
|
||||
Version: RttysVersion,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "log",
|
||||
Value: defaultLogPath,
|
||||
Usage: "log file path",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "log-level",
|
||||
Value: "info",
|
||||
Usage: "log level(debug, info, warn, error)",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "conf",
|
||||
Aliases: []string{"c"},
|
||||
Usage: "config file to load",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "addr-dev",
|
||||
Value: ":5912",
|
||||
Usage: "address to listen device",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "addr-user",
|
||||
Value: ":5913",
|
||||
Usage: "address to listen user",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "addr-http-proxy",
|
||||
Usage: "address to listen for HTTP proxy (default auto)",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "http-proxy-redir-url",
|
||||
Usage: "url to redirect for HTTP proxy",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "http-proxy-redir-domain",
|
||||
Usage: "domain for HTTP proxy set cookie",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "token",
|
||||
Aliases: []string{"t"},
|
||||
Usage: "token to use",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "dev-hook-url",
|
||||
Usage: "called when the device is connected",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "user-hook-url",
|
||||
Usage: "called when user accesses /connect/:devid, /cmd/:devid, /web/, or /web2/ APIs",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "local-auth",
|
||||
Value: true,
|
||||
Usage: "need auth for local",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "password",
|
||||
Usage: "web management password",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
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)",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "verbose",
|
||||
Aliases: []string{"V"},
|
||||
Usage: "more detailed output",
|
||||
},
|
||||
},
|
||||
Action: cmdAction,
|
||||
}
|
||||
cmd := &cli.Command{
|
||||
Name: "rttys",
|
||||
Usage: "The server side for rtty",
|
||||
Version: RttysVersion,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "log",
|
||||
Value: defaultLogPath,
|
||||
Usage: "log file path",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "log-level",
|
||||
Value: "info",
|
||||
Usage: "log level(debug, info, warn, error)",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "conf",
|
||||
Aliases: []string{"c"},
|
||||
Usage: "config file to load",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "addr-dev",
|
||||
Value: ":5912",
|
||||
Usage: "address to listen device",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "addr-user",
|
||||
Value: ":5913",
|
||||
Usage: "address to listen user",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "addr-http-proxy",
|
||||
Usage: "address to listen for HTTP proxy (default auto)",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "http-proxy-redir-url",
|
||||
Usage: "url to redirect for HTTP proxy",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "http-proxy-redir-domain",
|
||||
Usage: "domain for HTTP proxy set cookie",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "token",
|
||||
Aliases: []string{"t"},
|
||||
Usage: "token to use",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "dev-hook-url",
|
||||
Usage: "called when the device is connected",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "user-hook-url",
|
||||
Usage: "called when user accesses /connect/:devid, /cmd/:devid, /web/, or /web2/ APIs",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "local-auth",
|
||||
Value: true,
|
||||
Usage: "need auth for local",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "password",
|
||||
Usage: "web management password",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
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)",
|
||||
},
|
||||
|
||||
err := cmd.Run(context.Background(), os.Args)
|
||||
if err != nil {
|
||||
log.Fatal().Msg(err.Error())
|
||||
}
|
||||
// ---- OIDC Authentication (generic OIDC provider) ----
|
||||
&cli.BoolFlag{
|
||||
Name: "oidc-enabled",
|
||||
Usage: "enable OIDC authentication (OpenID Connect)",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "oidc-generic-client-id",
|
||||
Usage: "OIDC client ID (issued by the identity provider)",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "oidc-generic-client-secret",
|
||||
Usage: "OIDC client secret (read from OIDC_GENERIC_CLIENT_SECRET env by default)",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "oidc-generic-auth-url",
|
||||
Usage: "OIDC authorization endpoint URL",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "oidc-generic-token-url",
|
||||
Usage: "OIDC token endpoint URL",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "oidc-generic-redirect-url",
|
||||
Usage: "OIDC redirect/callback URL (must match one registered in IdP)",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "oidc-generic-scopes",
|
||||
Value: "openid profile email",
|
||||
Usage: "space-separated list of OIDC scopes",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "oidc-generic-allowed-users",
|
||||
Usage: "optional email whitelist for OIDC logins (exact emails or @domain, space/comma-separated)",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "oidc-generic-allowed-subs",
|
||||
Usage: "optional subject (sub) whitelist for OIDC logins (space/comma-separated)",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "oidc-generic-allowed-usernames",
|
||||
Usage: "optional username whitelist for OIDC logins (preferred_username/name, space/comma-separated)",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "oidc-generic-allowed-groups",
|
||||
Usage: "optional groups whitelist for OIDC logins (space/comma-separated)",
|
||||
},
|
||||
|
||||
&cli.BoolFlag{
|
||||
Name: "verbose",
|
||||
Aliases: []string{"V"},
|
||||
Usage: "more detailed output",
|
||||
},
|
||||
},
|
||||
Action: cmdAction,
|
||||
}
|
||||
|
||||
err := cmd.Run(context.Background(), os.Args)
|
||||
if err != nil {
|
||||
log.Fatal().Msg(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func cmdAction(c context.Context, cmd *cli.Command) error {
|
||||
defer logPanic()
|
||||
defer logPanic()
|
||||
|
||||
xlog.SetPath(cmd.String("log"))
|
||||
xlog.SetPath(cmd.String("log"))
|
||||
|
||||
switch cmd.String("log-level") {
|
||||
case "debug":
|
||||
zerolog.SetGlobalLevel(zerolog.DebugLevel)
|
||||
case "warn":
|
||||
zerolog.SetGlobalLevel(zerolog.WarnLevel)
|
||||
case "error":
|
||||
zerolog.SetGlobalLevel(zerolog.ErrorLevel)
|
||||
default:
|
||||
zerolog.SetGlobalLevel(zerolog.InfoLevel)
|
||||
}
|
||||
switch cmd.String("log-level") {
|
||||
case "debug":
|
||||
zerolog.SetGlobalLevel(zerolog.DebugLevel)
|
||||
case "warn":
|
||||
zerolog.SetGlobalLevel(zerolog.WarnLevel)
|
||||
case "error":
|
||||
zerolog.SetGlobalLevel(zerolog.ErrorLevel)
|
||||
default:
|
||||
zerolog.SetGlobalLevel(zerolog.InfoLevel)
|
||||
}
|
||||
|
||||
if cmd.Bool("verbose") {
|
||||
xlog.Verbose()
|
||||
}
|
||||
if cmd.Bool("verbose") {
|
||||
xlog.Verbose()
|
||||
}
|
||||
|
||||
log.Info().Msg("Go Version: " + runtime.Version())
|
||||
log.Info().Msgf("Go OS/Arch: %s/%s", runtime.GOOS, runtime.GOARCH)
|
||||
log.Info().Msg("Go Version: " + runtime.Version())
|
||||
log.Info().Msgf("Go OS/Arch: %s/%s", runtime.GOOS, runtime.GOARCH)
|
||||
|
||||
log.Info().Msg("Rttys Version: " + RttysVersion)
|
||||
log.Info().Msg("Rttys Version: " + RttysVersion)
|
||||
|
||||
if GitCommit != "" {
|
||||
log.Info().Msg("Git Commit: " + GitCommit)
|
||||
}
|
||||
if GitCommit != "" {
|
||||
log.Info().Msg("Git Commit: " + GitCommit)
|
||||
}
|
||||
|
||||
if BuildTime != "" {
|
||||
log.Info().Msg("Build Time: " + BuildTime)
|
||||
}
|
||||
if BuildTime != "" {
|
||||
log.Info().Msg("Build Time: " + BuildTime)
|
||||
}
|
||||
|
||||
if runtime.GOOS != "windows" {
|
||||
go signalHandle()
|
||||
}
|
||||
if runtime.GOOS != "windows" {
|
||||
go signalHandle()
|
||||
}
|
||||
|
||||
cfg := Config{
|
||||
AddrDev: ":5912",
|
||||
AddrUser: ":5913",
|
||||
LocalAuth: true,
|
||||
}
|
||||
cfg := Config{
|
||||
AddrDev: ":5912",
|
||||
AddrUser: ":5913",
|
||||
LocalAuth: true,
|
||||
}
|
||||
|
||||
err := cfg.Parse(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err := cfg.Parse(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
srv := &RttyServer{cfg: cfg}
|
||||
// ===== 打印完整配置(验证配置是否加载正确) =====
|
||||
{
|
||||
importJSON, _ := json.MarshalIndent(cfg, "", " ")
|
||||
log.Info().Msg("==== Loaded Configuration ====")
|
||||
log.Info().Msg(string(importJSON))
|
||||
log.Info().Msg("==============================")
|
||||
}
|
||||
|
||||
return srv.Run()
|
||||
srv := &RttyServer{cfg: cfg}
|
||||
|
||||
return srv.Run()
|
||||
}
|
||||
|
||||
func logPanic() {
|
||||
if r := recover(); r != nil {
|
||||
saveCrashLog(r, debug.Stack())
|
||||
os.Exit(2)
|
||||
}
|
||||
if r := recover(); r != nil {
|
||||
saveCrashLog(r, debug.Stack())
|
||||
os.Exit(2)
|
||||
}
|
||||
}
|
||||
|
||||
func saveCrashLog(p any, stack []byte) {
|
||||
log.Error().Msgf("%v", p)
|
||||
log.Error().Msg(string(stack))
|
||||
log.Error().Msgf("%v", p)
|
||||
log.Error().Msg(string(stack))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,476 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
oidc "github.com/coreos/go-oidc/v3/oidc"
|
||||
"github.com/fanjindong/go-cache"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gorilla/sessions"
|
||||
"github.com/rs/zerolog/log"
|
||||
"io"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"rttys/utils"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
// Session store for OAuth flow
|
||||
oauthStore *sessions.CookieStore
|
||||
// Global OIDC verifier
|
||||
oidcVerifier *oidc.IDTokenVerifier
|
||||
)
|
||||
|
||||
// Register OIDC routes
|
||||
func RegisterOIDCRoutes(r *gin.Engine, cfg *Config) {
|
||||
if !cfg.OIDCEnabled {
|
||||
return
|
||||
}
|
||||
|
||||
// ===== Initialize OIDC provider & ID token verifier =====
|
||||
issuer := strings.TrimSpace(cfg.OIDCGenericIssuer)
|
||||
if issuer == "" {
|
||||
log.Error().Msg("OIDC is enabled but issuer (OIDCGenericIssuer) is empty")
|
||||
return
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
//// Normalize issuer to always end with a single '/'
|
||||
//issuer = strings.TrimRight(issuer, "/") + "/"
|
||||
cfg.OIDCGenericIssuer = issuer
|
||||
provider, err := oidc.NewProvider(ctx, issuer)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to initialize OIDC provider")
|
||||
return
|
||||
}
|
||||
|
||||
oidcVerifier = provider.Verifier(&oidc.Config{
|
||||
ClientID: cfg.OIDCGenericClientID,
|
||||
// You can set SkipIssuerCheck, SkipClientIDCheck here if needed, but not recommended.
|
||||
})
|
||||
|
||||
// Initialize session store
|
||||
sessionSecret := generateRandomString(32)
|
||||
oauthStore = sessions.NewCookieStore([]byte(sessionSecret))
|
||||
|
||||
// Configure session options
|
||||
oauthStore.Options = &sessions.Options{
|
||||
Path: "/",
|
||||
MaxAge: 300, // 5 minutes, enough to complete the OAuth flow
|
||||
HttpOnly: true,
|
||||
SameSite: http.SameSiteLaxMode, // Important: allow cookies on cross-site navigation (OIDC redirect)
|
||||
Domain: "", // Empty means current host/domain will be used
|
||||
}
|
||||
|
||||
// OIDC auth routes (public, no existing auth required)
|
||||
r.GET("/auth/oidc/login", oidcLoginHandler(cfg))
|
||||
r.GET("/auth/oidc/callback", oidcCallbackHandler(cfg))
|
||||
}
|
||||
|
||||
// Start OIDC login
|
||||
func oidcLoginHandler(cfg *Config) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
|
||||
// Generate state and nonce
|
||||
nonce := generateRandomString(32)
|
||||
state := generateRandomString(32)
|
||||
|
||||
log.Info().Msgf("OIDC login initiated: nonce=%s, state=%s...", nonce[:10], state[:10])
|
||||
|
||||
// Save to session
|
||||
session, _ := oauthStore.Get(c.Request, "oidc-session")
|
||||
session.Values["state"] = state
|
||||
session.Values["nonce"] = nonce
|
||||
|
||||
if err := session.Save(c.Request, c.Writer); err != nil {
|
||||
log.Error().Err(err).Msg("Failed to save OIDC session")
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save session"})
|
||||
return
|
||||
}
|
||||
|
||||
// Build authorization URL
|
||||
params := url.Values{}
|
||||
params.Add("client_id", cfg.OIDCGenericClientID)
|
||||
params.Add("redirect_uri", cfg.OIDCGenericRedirectURL)
|
||||
params.Add("response_type", "code")
|
||||
params.Add("scope", strings.Join(cfg.OIDCGenericScopes, " "))
|
||||
params.Add("state", state)
|
||||
params.Add("nonce", nonce)
|
||||
|
||||
authURL := cfg.OIDCGenericAuthURL + "?" + params.Encode()
|
||||
log.Info().Msgf("OIDC login redirect=%s", authURL[:100]+"...")
|
||||
c.Redirect(http.StatusFound, authURL)
|
||||
}
|
||||
}
|
||||
|
||||
// Handle OIDC callback
|
||||
func oidcCallbackHandler(cfg *Config) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// Get session
|
||||
session, err := oauthStore.Get(c.Request, "oidc-session")
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to get OIDC session")
|
||||
c.Redirect(http.StatusFound, "/?error=session_error")
|
||||
return
|
||||
}
|
||||
|
||||
// Validate state
|
||||
state := c.Query("state")
|
||||
savedState, ok := session.Values["state"].(string)
|
||||
if !ok || state != savedState {
|
||||
log.Warn().Msg("OIDC state mismatch")
|
||||
c.Redirect(http.StatusFound, "/?error=invalid_state")
|
||||
return
|
||||
}
|
||||
|
||||
// Check OAuth error
|
||||
if errorMsg := c.Query("error"); errorMsg != "" {
|
||||
errorDesc := c.Query("error_description")
|
||||
log.Warn().Msgf("OIDC error: %s - %s", errorMsg, errorDesc)
|
||||
c.Redirect(http.StatusFound, "/?error="+errorMsg)
|
||||
return
|
||||
}
|
||||
|
||||
// Read authorization code
|
||||
code := c.Query("code")
|
||||
if code == "" {
|
||||
c.Redirect(http.StatusFound, "/?error=no_code")
|
||||
return
|
||||
}
|
||||
|
||||
// Exchange authorization code for tokens
|
||||
tokens, err := exchangeCodeForTokens(cfg, code)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to exchange code for tokens")
|
||||
c.Redirect(http.StatusFound, "/?error=token_exchange_failed")
|
||||
return
|
||||
}
|
||||
|
||||
// Extract user info
|
||||
var userEmail string
|
||||
var userName string
|
||||
|
||||
// Standard OIDC – verify and parse ID token
|
||||
rawIDToken, ok := tokens["id_token"].(string)
|
||||
if !ok {
|
||||
log.Error().Msg("No ID token in response")
|
||||
c.Redirect(http.StatusFound, "/?error=no_id_token")
|
||||
return
|
||||
}
|
||||
|
||||
if oidcVerifier == nil {
|
||||
log.Error().Msg("OIDC verifier is not initialized")
|
||||
c.Redirect(http.StatusFound, "/?error=server_config")
|
||||
return
|
||||
}
|
||||
|
||||
// ==== Signature + standard claims verification ====
|
||||
idToken, err := oidcVerifier.Verify(c.Request.Context(), rawIDToken)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to verify ID token signature/claims")
|
||||
c.Redirect(http.StatusFound, "/?error=invalid_token")
|
||||
return
|
||||
}
|
||||
|
||||
// Decode claims into a map
|
||||
claims := map[string]interface{}{}
|
||||
if err := idToken.Claims(&claims); err != nil {
|
||||
log.Error().Err(err).Msg("Failed to parse ID token claims")
|
||||
c.Redirect(http.StatusFound, "/?error=invalid_token")
|
||||
return
|
||||
}
|
||||
|
||||
// Pretty-print all claims as JSON for debugging
|
||||
if claimsJSON, err := json.MarshalIndent(claims, "", " "); err == nil {
|
||||
log.Info().Msg("========== OIDC ID Token Claims ==========")
|
||||
log.Info().Msg(string(claimsJSON))
|
||||
log.Info().Msg("==========================================")
|
||||
} else {
|
||||
log.Warn().Err(err).Msg("Failed to marshal OIDC claims to JSON")
|
||||
}
|
||||
|
||||
// Validate nonce
|
||||
if savedNonce, ok := session.Values["nonce"].(string); ok {
|
||||
if claims["nonce"] != savedNonce {
|
||||
log.Warn().Msg("OIDC nonce mismatch")
|
||||
c.Redirect(http.StatusFound, "/?error=invalid_nonce")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
sub, _ := claims["sub"].(string)
|
||||
userEmail, _ = claims["email"].(string)
|
||||
userName, _ = claims["name"].(string)
|
||||
if userName == "" {
|
||||
// Fallback: use email or sub as display name
|
||||
if userEmail != "" {
|
||||
userName = userEmail
|
||||
} else {
|
||||
userName = sub
|
||||
}
|
||||
}
|
||||
|
||||
// sub is required by OIDC spec and should never be empty
|
||||
if sub == "" {
|
||||
log.Error().Msg("OIDC token is missing 'sub' claim")
|
||||
c.Redirect(http.StatusFound, "/?error=user_info_failed")
|
||||
return
|
||||
}
|
||||
|
||||
log.Info().Msgf("OIDC login successful: email=%s", userEmail)
|
||||
|
||||
// ====== OIDC whitelist enforcement ======
|
||||
if !isOIDCUserAllowed(cfg, claims) {
|
||||
log.Warn().Msgf("OIDC user not allowed by whitelist rules, sub=%v, email=%v", claims["sub"], claims["email"])
|
||||
c.Redirect(http.StatusFound, "/?error=authorization")
|
||||
return
|
||||
}
|
||||
|
||||
// Create application session
|
||||
sid := utils.GenUniqueID()
|
||||
httpSessions.Set(sid, gin.H{
|
||||
"email": userEmail,
|
||||
"name": userName,
|
||||
"oidc": true,
|
||||
}, cache.WithEx(httpSessionExpire))
|
||||
|
||||
c.SetCookie("sid", sid, 0, "", "", cfg.SslCert != "", true)
|
||||
|
||||
// Clean up OAuth session
|
||||
session.Options.MaxAge = -1
|
||||
session.Save(c.Request, c.Writer)
|
||||
|
||||
// Redirect to home page
|
||||
c.Redirect(http.StatusFound, "/")
|
||||
}
|
||||
}
|
||||
|
||||
// Exchange authorization code for tokens
|
||||
func exchangeCodeForTokens(cfg *Config, code string) (map[string]interface{}, error) {
|
||||
data := url.Values{}
|
||||
data.Set("code", code)
|
||||
data.Set("client_id", cfg.OIDCGenericClientID)
|
||||
data.Set("client_secret", cfg.OIDCGenericClientSecret)
|
||||
data.Set("redirect_uri", cfg.OIDCGenericRedirectURL)
|
||||
data.Set("grant_type", "authorization_code")
|
||||
|
||||
req, err := http.NewRequest("POST", cfg.OIDCGenericTokenURL, strings.NewReader(data.Encode()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.Header.Set("Accept", "application/json")
|
||||
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("token request failed: %s", string(body))
|
||||
}
|
||||
|
||||
var result map[string]interface{}
|
||||
if err := json.Unmarshal(body, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Generate a random string with given length
|
||||
func generateRandomString(length int) string {
|
||||
b := make([]byte, length)
|
||||
rand.Read(b)
|
||||
return base64.URLEncoding.EncodeToString(b)[:length]
|
||||
}
|
||||
|
||||
func isOIDCUserAllowed(cfg *Config, claims map[string]interface{}) bool {
|
||||
email, _ := claims["email"].(string)
|
||||
sub, _ := claims["sub"].(string)
|
||||
preferredUsername, _ := claims["preferred_username"].(string)
|
||||
name, _ := claims["name"].(string)
|
||||
|
||||
// 1) Email whitelist
|
||||
if len(cfg.OIDCGenericAllowedUsers) > 0 {
|
||||
if !isEmailAllowed(cfg.OIDCGenericAllowedUsers, email) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 2) Sub (subject) whitelist
|
||||
if len(cfg.OIDCGenericAllowedSubs) > 0 {
|
||||
if !contains(cfg.OIDCGenericAllowedSubs, sub) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 3) Username whitelist (preferred_username > name)
|
||||
if len(cfg.OIDCGenericAllowedUsernames) > 0 {
|
||||
u := preferredUsername
|
||||
if u == "" {
|
||||
u = name
|
||||
}
|
||||
if !contains(cfg.OIDCGenericAllowedUsernames, u) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 4) Groups whitelist
|
||||
if len(cfg.OIDCGenericAllowedGroups) > 0 {
|
||||
groups := extractStringSlice(claims["groups"])
|
||||
if !intersects(groups, cfg.OIDCGenericAllowedGroups) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// Email whitelist check rules:
|
||||
// - Exact match (case-insensitive)
|
||||
// - Simple domain match: entries starting with "@example.com" or "*@example.com"
|
||||
// mean "allow all users under this domain"
|
||||
func isEmailAllowed(allowed []string, email string) bool {
|
||||
if len(allowed) == 0 {
|
||||
return true
|
||||
}
|
||||
e := strings.ToLower(strings.TrimSpace(email))
|
||||
for _, raw := range allowed {
|
||||
a := strings.ToLower(strings.TrimSpace(raw))
|
||||
if a == "" {
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(a, "*@") || strings.HasPrefix(a, "@") {
|
||||
// Domain match
|
||||
dom := strings.TrimPrefix(a, "*@")
|
||||
dom = strings.TrimPrefix(dom, "@")
|
||||
if strings.HasSuffix(e, "@"+dom) {
|
||||
return true
|
||||
}
|
||||
continue
|
||||
}
|
||||
// Exact match
|
||||
if e == a {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// contains reports whether slice contains the given value v.
|
||||
// Comparison is done after trimming spaces on both sides.
|
||||
func contains(slice []string, v string) bool {
|
||||
v = strings.TrimSpace(v)
|
||||
if v == "" {
|
||||
return false
|
||||
}
|
||||
for _, s := range slice {
|
||||
if strings.TrimSpace(s) == v {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// extractStringSlice tries to normalize a generic claim value into a []string.
|
||||
//
|
||||
// It supports:
|
||||
// - []string
|
||||
// - []interface{} (only string elements are kept)
|
||||
// - string (split by comma and/or whitespace)
|
||||
// Any other type will result in an empty slice.
|
||||
func extractStringSlice(v interface{}) []string {
|
||||
if v == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch vv := v.(type) {
|
||||
case []string:
|
||||
// Return a shallow copy to avoid accidental modification.
|
||||
out := make([]string, 0, len(vv))
|
||||
for _, s := range vv {
|
||||
s = strings.TrimSpace(s)
|
||||
if s != "" {
|
||||
out = append(out, s)
|
||||
}
|
||||
}
|
||||
return out
|
||||
|
||||
case []interface{}:
|
||||
out := make([]string, 0, len(vv))
|
||||
for _, item := range vv {
|
||||
s, ok := item.(string)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
s = strings.TrimSpace(s)
|
||||
if s != "" {
|
||||
out = append(out, s)
|
||||
}
|
||||
}
|
||||
return out
|
||||
|
||||
case string:
|
||||
// Allow comma- or whitespace-separated group lists.
|
||||
s := strings.TrimSpace(vv)
|
||||
if s == "" {
|
||||
return nil
|
||||
}
|
||||
// Replace commas with spaces, then split on whitespace.
|
||||
s = strings.ReplaceAll(s, ",", " ")
|
||||
parts := strings.Fields(s)
|
||||
out := make([]string, 0, len(parts))
|
||||
for _, p := range parts {
|
||||
p = strings.TrimSpace(p)
|
||||
if p != "" {
|
||||
out = append(out, p)
|
||||
}
|
||||
}
|
||||
return out
|
||||
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// intersects reports whether slice a and b share at least one common element.
|
||||
// Matching is done after trimming spaces on both sides.
|
||||
func intersects(a, b []string) bool {
|
||||
if len(a) == 0 || len(b) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
m := make(map[string]struct{}, len(a))
|
||||
for _, s := range a {
|
||||
s = strings.TrimSpace(s)
|
||||
if s == "" {
|
||||
continue
|
||||
}
|
||||
m[s] = struct{}{}
|
||||
}
|
||||
|
||||
for _, s := range b {
|
||||
s = strings.TrimSpace(s)
|
||||
if s == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := m[s]; ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
@@ -27,7 +27,9 @@
|
||||
"signOut": "Sign Out",
|
||||
"authOptions": "Authentication Options",
|
||||
"ldapAuth": "Enter username and password for LDAP authentication",
|
||||
"webManagementAuth": "Leave username empty and use web management password"
|
||||
"webManagementAuth": "Leave username empty and use web management password",
|
||||
"or": "OR",
|
||||
"loginWithOidc": "Log in with OIDC"
|
||||
},
|
||||
"device": {
|
||||
"devices": "Devices",
|
||||
|
||||
@@ -27,7 +27,9 @@
|
||||
"signOut": "退出",
|
||||
"authOptions": "认证方式",
|
||||
"ldapAuth": "输入用户名和密码进行LDAP认证",
|
||||
"webManagementAuth": "留空用户名并使用Web管理密码"
|
||||
"webManagementAuth": "留空用户名并使用Web管理密码",
|
||||
"or": "或",
|
||||
"loginWithOidc": "使用OIDC登录"
|
||||
},
|
||||
"device": {
|
||||
"devices": "设备数",
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
/*
|
||||
* @Author: LPY
|
||||
* @Date: 2025-06-09 16:37:00
|
||||
* @LastEditors: CU-Jon
|
||||
* @LastEditTime: 2025-09-26 14:02:57 EDT
|
||||
* @FilePath: \glkvm-cloud\web-ui\src\models\user.ts
|
||||
* @LastEditors: LPY
|
||||
* @LastEditTime: 2025-11-12 16:19:19
|
||||
* @FilePath: \glkvm-cloud\ui\src\models\user.ts
|
||||
* @Description: 用户相关类型声明
|
||||
*/
|
||||
|
||||
@@ -18,4 +18,5 @@ export interface LoginParams {
|
||||
export interface AuthConfig {
|
||||
ldapEnabled: boolean;
|
||||
legacyPassword: boolean;
|
||||
oidcEnabled: boolean;
|
||||
}
|
||||
@@ -2,11 +2,12 @@
|
||||
* @Author: LPY
|
||||
* @Date: 2025-05-30 09:54:42
|
||||
* @LastEditors: LPY
|
||||
* @LastEditTime: 2025-08-22 12:09:36
|
||||
* @FilePath: \glkvm-cloud\web-ui\src\router\whiteList.ts
|
||||
* @LastEditTime: 2025-11-12 16:36:23
|
||||
* @FilePath: \glkvm-cloud\ui\src\router\whiteList.ts
|
||||
* @Description: 路由白名单页
|
||||
*/
|
||||
export default [
|
||||
'/login',
|
||||
'/error',
|
||||
'/auth/oidc/login',
|
||||
]
|
||||
@@ -1,9 +1,9 @@
|
||||
<!--
|
||||
* @Author: LPY
|
||||
* @Date: 2025-05-30 10:48:43
|
||||
* @LastEditors: CU-Jon
|
||||
* @LastEditTime: 2025-09-26 14:02:57 EDT
|
||||
* @FilePath: \glkvm-cloud\web-ui\src\views\login\loginPage.vue
|
||||
* @LastEditors: LPY
|
||||
* @LastEditTime: 2025-11-12 17:01:59
|
||||
* @FilePath: \glkvm-cloud\ui\src\views\login\loginPage.vue
|
||||
* @Description: 登录页面
|
||||
-->
|
||||
<template>
|
||||
@@ -13,6 +13,7 @@
|
||||
<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
|
||||
@@ -28,7 +29,11 @@
|
||||
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"/>
|
||||
<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) -->
|
||||
@@ -72,6 +77,16 @@
|
||||
<BaseButton medium type="primary" style="width: 100%;margin-top: 16px;" :loading="state.loading" @click="handleLogin">
|
||||
{{ $t('login.signIn') }}
|
||||
</BaseButton>
|
||||
|
||||
<div v-if="isOidcEnabled" class="google-login-box">
|
||||
<a-divider style="border-color: var(--gl-color-line-divider1);color: var(--gl-color-text-level3);font-weight: normal;">
|
||||
{{ $t('login.or') }}
|
||||
</a-divider>
|
||||
<BaseButton medium class="google-login-btn" :loading="state.oidcLoading" @click="handleLoginWithOidc">
|
||||
<!-- <BaseSvg name="gl-icon-google" :size="20" style="margin-right: 10px;"/> -->
|
||||
{{ $t('login.loginWithOidc') }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
</LoginBox>
|
||||
</BaseWhitePage>
|
||||
</template>
|
||||
@@ -105,12 +120,18 @@ const isLdapEnabled = computed(() => {
|
||||
return authConfig.value?.ldapEnabled === true
|
||||
})
|
||||
|
||||
const state = reactive<{formModel: LoginParams, loading: boolean}>({
|
||||
// 计算是否允许OIDC认证
|
||||
const isOidcEnabled = computed(() => {
|
||||
return authConfig.value?.oidcEnabled === true
|
||||
})
|
||||
|
||||
const state = reactive<{formModel: LoginParams, loading: boolean, oidcLoading: boolean}>({
|
||||
formModel: {
|
||||
username: '',
|
||||
password: '',
|
||||
},
|
||||
loading: false,
|
||||
oidcLoading: false,
|
||||
})
|
||||
|
||||
const showTooltip = ref(false)
|
||||
@@ -136,7 +157,7 @@ onMounted(async () => {
|
||||
} catch (error) {
|
||||
console.error('Failed to load auth config:', error)
|
||||
// 回退 - 无LDAP可用 (Fallback - no LDAP available)
|
||||
authConfig.value = { ldapEnabled: false, legacyPassword: true }
|
||||
authConfig.value = { ldapEnabled: false, legacyPassword: true, oidcEnabled: false }
|
||||
}
|
||||
})
|
||||
|
||||
@@ -149,7 +170,7 @@ const handleLogin = () => {
|
||||
const loginData: LoginParams = {
|
||||
username: state.formModel.username,
|
||||
password: state.formModel.password,
|
||||
authMethod: (state.formModel.username && authConfig.value?.ldapEnabled) ? 'ldap' : 'legacy'
|
||||
authMethod: (state.formModel.username && authConfig.value?.ldapEnabled) ? 'ldap' : 'legacy',
|
||||
}
|
||||
|
||||
await useUserStore().login(loginData)
|
||||
@@ -178,6 +199,13 @@ const handleLogin = () => {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// oidc登录按钮
|
||||
const handleLoginWithOidc = () => {
|
||||
state.oidcLoading = true
|
||||
const baseUrl = window.location.origin
|
||||
window.location.href = `${baseUrl}/auth/oidc/login`
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@@ -235,4 +263,23 @@ const handleLogin = () => {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.google-login-box {
|
||||
width: 100%;
|
||||
|
||||
.google-login-btn {
|
||||
width: 100%;
|
||||
color: var(--gl-color-text-google);
|
||||
background-color: var(--gl-color-bg-google);
|
||||
border-color: var(--gl-color-line-google);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
height: 40px;
|
||||
border-radius: 64px;
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
+12
-8
@@ -23,35 +23,39 @@ export default defineConfig(({ mode }) => {
|
||||
port: 3011,
|
||||
proxy: {
|
||||
'/devs': {
|
||||
target: 'https://106.55.158.199',
|
||||
target: 'https://oidc.clanxie.cn',
|
||||
secure: false,
|
||||
},
|
||||
'/signin': {
|
||||
target: 'https://106.55.158.199',
|
||||
target: 'https://oidc.clanxie.cn',
|
||||
secure: false,
|
||||
},
|
||||
'/signout': {
|
||||
target: 'https://106.55.158.199',
|
||||
target: 'https://oidc.clanxie.cn',
|
||||
secure: false,
|
||||
},
|
||||
'/alive': {
|
||||
target: 'https://106.55.158.199',
|
||||
target: 'https://oidc.clanxie.cn',
|
||||
secure: false,
|
||||
},
|
||||
'/get': {
|
||||
target: 'https://106.55.158.199',
|
||||
target: 'https://oidc.clanxie.cn',
|
||||
secure: false,
|
||||
},
|
||||
'^/cmd/.*': {
|
||||
target: 'https://106.55.158.199',
|
||||
target: 'https://oidc.clanxie.cn',
|
||||
secure: false,
|
||||
},
|
||||
'^/connect/.*': {
|
||||
ws: true,
|
||||
target: 'https://106.55.158.199',
|
||||
target: 'https://oidc.clanxie.cn',
|
||||
},
|
||||
'^/web/*': {
|
||||
target: 'https://106.55.158.199',
|
||||
target: 'https://oidc.clanxie.cn',
|
||||
},
|
||||
'/auth-config': {
|
||||
target: 'https://oidc.clanxie.cn',
|
||||
secure: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user