package proxy import ( "fmt" "net" "net/http" "net/http/httputil" "net/url" "strings" "instance-manager/internal/auth" "instance-manager/internal/config" "instance-manager/internal/db" "instance-manager/internal/instance" ) type Proxy struct { db *db.Database manager *instance.Manager auth *auth.Manager cfg config.InstanceConfig } func (p *Proxy) BaseDomain() string { return p.cfg.BaseDomain } func New(db *db.Database, manager *instance.Manager, authMgr *auth.Manager, cfg config.InstanceConfig) *Proxy { return &Proxy{ db: db, manager: manager, auth: authMgr, cfg: cfg, } } func (p *Proxy) IsPrivateEndpoint(path string) bool { for _, pattern := range p.cfg.PrivateEndpoints { if matchWildcard(pattern, path) { return true } } return false } func matchWildcard(pattern, path string) bool { pattern = strings.TrimPrefix(pattern, "/") path = strings.TrimPrefix(path, "/") if strings.HasSuffix(pattern, "/*") { prefix := strings.TrimSuffix(pattern, "/*") return strings.HasPrefix(path, prefix) || path == strings.TrimPrefix(prefix, "/") } return pattern == path } func (p *Proxy) getPort(r *http.Request) string { host := r.Host if idx := strings.LastIndex(host, ":"); idx != -1 { return host[idx+1:] } if r.TLS != nil { return "443" } port := "80" if l, ok := r.Context().Value(http.LocalAddrContextKey).(net.Addr); ok { _, p, _ := net.SplitHostPort(l.String()) port = p } return port } // SubdomainRouter returns a middleware that intercepts requests for // . and proxies them to that user's instance. // Requests to the bare base domain pass through to the main app. func (p *Proxy) SubdomainRouter(baseDomain string) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { host := strings.TrimSuffix(r.Host, ":") // Strip port from host if present if idx := strings.LastIndex(host, ":"); idx != -1 { host = host[:idx] } // Check if this is a subdomain request expectedSuffix := "." + baseDomain if strings.HasSuffix(host, expectedSuffix) { username := strings.TrimSuffix(host, expectedSuffix) p.proxyToUser(w, r, username) return } // Pass through to main app (localhost / bare domain) next.ServeHTTP(w, r) }) } } func (p *Proxy) proxyToUser(w http.ResponseWriter, r *http.Request, username string) { user, err := p.db.GetUserByUsername(username) if err != nil || user == nil { http.Error(w, "User not found", http.StatusNotFound) return } if p.IsPrivateEndpoint(r.URL.Path) { sess, _ := p.auth.GetSession(r) if sess == nil || sess.UserID != user.ID { port := p.getPort(r) var mainBase string var subBase string if port == "80" || port == "443" { mainBase = fmt.Sprintf("http://%s", p.cfg.BaseDomain) subBase = fmt.Sprintf("http://%s.%s", username, p.cfg.BaseDomain) } else { mainBase = fmt.Sprintf("http://%s:%s", p.cfg.BaseDomain, port) subBase = fmt.Sprintf("http://%s.%s:%s", username, p.cfg.BaseDomain, port) } loginURL := fmt.Sprintf("%s/login?redirect=%s", mainBase, url.QueryEscape(subBase+r.URL.Path)) http.Redirect(w, r, loginURL, http.StatusFound) return } } inst, err := p.manager.GetInstance(user) if err != nil || inst == nil { http.Error(w, "Instance not found", http.StatusNotFound) return } if inst.Status != "running" { http.Error(w, "Instance is not running", http.StatusServiceUnavailable) return } targetURL, err := url.Parse(inst.URL) if err != nil { http.Error(w, "Invalid instance URL", http.StatusInternalServerError) return } proxy := httputil.NewSingleHostReverseProxy(targetURL) proxy.ErrorHandler = func(w http.ResponseWriter, req *http.Request, err error) { http.Error(w, fmt.Sprintf("Proxy error: %v", err), http.StatusBadGateway) } originalURL := r.URL r.URL.Scheme = targetURL.Scheme r.URL.Host = targetURL.Host r.Header.Set("X-Forwarded-Host", r.Host) r.Header.Set("X-Forwarded-Proto", r.URL.Scheme) r.Host = targetURL.Host proxy.ServeHTTP(w, r) r.URL = originalURL } func (p *Proxy) ProxyHandler(userID int64, pathPrefix string) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { user, err := p.db.GetUserByID(userID) if err != nil || user == nil { http.Error(w, "User not found", http.StatusNotFound) return } inst, err := p.manager.GetInstance(user) if err != nil || inst == nil { http.Error(w, "Instance not found", http.StatusNotFound) return } if inst.Status != "running" { http.Error(w, "Instance is not running", http.StatusServiceUnavailable) return } targetURL, err := url.Parse(inst.URL) if err != nil { http.Error(w, "Invalid instance URL", http.StatusInternalServerError) return } remainingPath := strings.TrimPrefix(r.URL.Path, pathPrefix) if remainingPath == "" { remainingPath = "/" } proxy := httputil.NewSingleHostReverseProxy(targetURL) proxy.ErrorHandler = func(w http.ResponseWriter, req *http.Request, err error) { http.Error(w, fmt.Sprintf("Proxy error: %v", err), http.StatusBadGateway) } originalURL := r.URL r.URL.Scheme = targetURL.Scheme r.URL.Host = targetURL.Host r.URL.Path = remainingPath r.Header.Set("X-Forwarded-Host", r.Host) r.Host = targetURL.Host proxy.ServeHTTP(w, r) r.URL = originalURL } }