119 lines
3.7 KiB
Go
119 lines
3.7 KiB
Go
package api
|
|
|
|
import (
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"vpn-platform/backend/internal/model"
|
|
"vpn-platform/backend/internal/nginxconfig"
|
|
"vpn-platform/backend/internal/nginxgateway"
|
|
"vpn-platform/backend/internal/service"
|
|
"vpn-platform/backend/internal/store"
|
|
"vpn-platform/backend/internal/tlsservice"
|
|
)
|
|
|
|
// Only a single OpenVPN WebAuth instance may own /oauth2/ on the management
|
|
// hostname. Never route an arbitrary user-supplied address to a local port.
|
|
func authGatewayPort(publicBase string) (int, error) {
|
|
publicBase = strings.TrimRight(strings.TrimSpace(publicBase), "/")
|
|
var tunnels []model.Tunnel
|
|
if err := store.DB.Where("protocol = ? AND state = ?", "openvpn", "running").Find(&tunnels).Error; err != nil {
|
|
return 0, err
|
|
}
|
|
port := 0
|
|
for _, tunnel := range tunnels {
|
|
cfg, err := store.TunnelConfig(&tunnel)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
if service.Str(cfg, "auth_mode") != "oidc" || !strings.EqualFold(strings.TrimRight(service.Str(cfg, "oidc_base_url"), "/"), publicBase) {
|
|
continue
|
|
}
|
|
if port != 0 {
|
|
return 0, &gatewayConflict{}
|
|
}
|
|
port = int(service.Num(cfg, "oidc_http_port", 0))
|
|
}
|
|
return port, nil
|
|
}
|
|
|
|
type gatewayConflict struct{}
|
|
|
|
func (*gatewayConflict) Error() string {
|
|
return "当前域名存在多条运行中的 OIDC 专线,请先保留一条"
|
|
}
|
|
|
|
// RefreshManagedGatewayOnStart keeps the installed route allowlist in sync
|
|
// with the binary after upgrades. It only touches this application's managed
|
|
// Nginx site; RefreshAuth validates it and rolls back on failed reloads.
|
|
func (s *Server) RefreshManagedGatewayOnStart() error {
|
|
s.gatewayMu.Lock()
|
|
defer s.gatewayMu.Unlock()
|
|
status := tlsservice.Status()
|
|
if status.Mode != "nginx" || status.RestartRequired || status.Domain == "" || status.PublicBase == "" {
|
|
return nil
|
|
}
|
|
scheme := status.Scheme
|
|
if scheme == "" {
|
|
scheme = "https"
|
|
}
|
|
stage := nginxgateway.Stage()
|
|
if scheme == "https" && (!status.Valid || stage != "https") {
|
|
return nil
|
|
}
|
|
if scheme == "http" && stage == "none" {
|
|
return nil
|
|
}
|
|
port, err := authGatewayPort(status.PublicBase)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nginxgateway.RefreshAuthEntry(nginxconfig.RenderOptions{
|
|
Domain: status.Domain, Scheme: scheme, Port: status.Port,
|
|
}, s.router.Routes(), port)
|
|
}
|
|
|
|
func (s *Server) syncAuthGatewayForTunnel(t *model.Tunnel) error {
|
|
if t.Protocol != "openvpn" || s.router == nil {
|
|
return nil
|
|
}
|
|
return s.RefreshManagedGatewayOnStart()
|
|
}
|
|
|
|
func (s *Server) identityGateway(c *gin.Context) {
|
|
s.gatewayMu.Lock()
|
|
defer s.gatewayMu.Unlock()
|
|
status := tlsservice.Status()
|
|
scheme := status.Scheme
|
|
if scheme == "" {
|
|
scheme = "https"
|
|
}
|
|
if status.Mode != "nginx" || status.RestartRequired || status.Domain == "" || status.PublicBase == "" {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "请先完成公网入口配置"})
|
|
return
|
|
}
|
|
stage := nginxgateway.Stage()
|
|
if scheme == "https" && (!status.Valid || stage != "https") {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "请先完成 Nginx HTTPS 入口配置"})
|
|
return
|
|
}
|
|
if scheme == "http" && stage == "none" {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "请先启用 HTTP 公网入口"})
|
|
return
|
|
}
|
|
port, err := authGatewayPort(status.PublicBase)
|
|
if err != nil {
|
|
c.JSON(400, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
if err := nginxgateway.RefreshAuthEntry(nginxconfig.RenderOptions{
|
|
Domain: status.Domain, Scheme: scheme, Port: status.Port,
|
|
}, s.router.Routes(), port); err != nil {
|
|
c.JSON(500, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
store.Audit(c.GetString("username"), "identity_gateway_updated", status.PublicBase, "")
|
|
c.JSON(200, gin.H{"ok": true, "identity_route": true, "auth_route": port != 0, "auth_port": port, "public_base": status.PublicBase})
|
|
}
|