benzcloud-server/internal/core/proxy/proxy.go

106 lines
3.0 KiB
Go

package proxy
import (
"fmt"
"net/http"
"net/http/httputil"
"net/url"
"strings"
"github.com/benzjeremy/benzcloud-server/internal/core/plugins"
)
// Router handles incoming HTTP requests and dispatches them based on Subdomain/Host.
type Router struct {
baseDomain string
serverLocalIP string
serverToken string
pluginMgr *plugins.Manager
mainHandler http.Handler
driveHandler http.Handler
proxies map[int]*httputil.ReverseProxy
}
// NewRouter creates the virtual host dispatcher.
func NewRouter(baseDomain, serverLocalIP, serverToken string, pluginMgr *plugins.Manager, mainHandler, driveHandler http.Handler) *Router {
return &Router{
baseDomain: strings.ToLower(strings.Trim(baseDomain, ".")),
serverLocalIP: serverLocalIP,
serverToken: serverToken,
pluginMgr: pluginMgr,
mainHandler: mainHandler,
driveHandler: driveHandler,
proxies: make(map[int]*httputil.ReverseProxy),
}
}
func (rt *Router) getReverseProxy(port int) *httputil.ReverseProxy {
if p, exists := rt.proxies[port]; exists {
return p
}
targetURL, _ := url.Parse(fmt.Sprintf("http://127.0.0.1:%d", port))
proxy := httputil.NewSingleHostReverseProxy(targetURL)
// Add server token header for internal authentication
origDirector := proxy.Director
proxy.Director = func(req *http.Request) {
origDirector(req)
req.Header.Set("X-BenzCloud-Token", rt.serverToken)
}
rt.proxies[port] = proxy
return proxy
}
func (rt *Router) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Security Headers
w.Header().Set("X-Content-Type-Options", "nosniff")
w.Header().Set("X-Frame-Options", "SAMEORIGIN")
w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin")
host := strings.ToLower(r.Host)
if colonIdx := strings.Index(host, ":"); colonIdx != -1 {
host = host[:colonIdx]
}
// 1. Direct IP or localhost or root baseDomain -> Main Handler (Setup / Dashboard / API)
if host == "localhost" || host == "127.0.0.1" || host == rt.serverLocalIP || host == "10.42.0.1" || host == rt.baseDomain {
rt.mainHandler.ServeHTTP(w, r)
return
}
// 2. Subdomain check: host must end with .baseDomain
suffix := "." + rt.baseDomain
if !strings.HasSuffix(host, suffix) {
// Anti-DNS Rebinding: Unknown external Host header
http.Error(w, "403 Forbidden: Invalid Host Header (Anti-DNS-Rebinding Protection)", http.StatusForbidden)
return
}
subdomain := strings.TrimSuffix(host, suffix)
// 3. System Subdomains
switch subdomain {
case "drive":
if rt.driveHandler != nil {
rt.driveHandler.ServeHTTP(w, r)
return
}
case "vpn":
// Direct to VPN status on main handler
r.URL.Path = "/#vpn"
rt.mainHandler.ServeHTTP(w, r)
return
}
// 4. Plugin Subdomains (mail, chat, or custom web)
if rt.pluginMgr != nil {
if port, ok := rt.pluginMgr.GetTargetForSubdomain(subdomain); ok {
proxy := rt.getReverseProxy(port)
proxy.ServeHTTP(w, r)
return
}
}
// Subdomain not found
http.Error(w, fmt.Sprintf("404 BenzCloud: Subdomain '%s.%s' is not mapped to any active service or plugin.", subdomain, rt.baseDomain), http.StatusNotFound)
}