package handlers import ( "fmt" "log" "net/http" "net/http/httputil" "net/url" "regexp" "strings" "github.com/go-chi/chi/v5" ) // instanceUsernamePattern matches valid instance slugs. var instanceUsernamePattern = regexp.MustCompile(`^[a-z0-9-]+$`) // handleInstanceProxy enforces account ownership for /i/, then // reverse-proxies to that customer stack's web service. func (a *App) handleInstanceProxy(w http.ResponseWriter, r *http.Request) { slug := chi.URLParam(r, "username") if slug == "" { http.Error(w, "forbidden", http.StatusForbidden) return } if !instanceUsernamePattern.MatchString(slug) { http.Error(w, "forbidden", http.StatusForbidden) return } acct, identity, err := a.currentAccount(r) if err != nil || acct == nil { http.Redirect(w, r, a.cfg.IdentityURL, http.StatusSeeOther) return } inst, err := a.accounts.InstanceBySlug(r.Context(), slug) if err != nil || inst.AccountID != acct.ID { log.Printf("instance proxy: denied %s access to /i/%s", accountDisplay(acct, identity), slug) http.Error(w, "forbidden", http.StatusForbidden) return } backendHost := fmt.Sprintf("web.%s", inst.StackName) backendURL := &url.URL{ Scheme: "http", Host: backendHost + ":3001", Path: "/", } prefix := "/i/" + slug pathSuffix := strings.TrimPrefix(r.URL.Path, prefix) if pathSuffix == "" { pathSuffix = "/" } if !strings.HasPrefix(pathSuffix, "/") { pathSuffix = "/" + pathSuffix } proxy := httputil.NewSingleHostReverseProxy(backendURL) proxy.Director = func(req *http.Request) { req.URL.Scheme = backendURL.Scheme req.URL.Host = backendURL.Host req.URL.Path = pathSuffix req.Host = backendURL.Host } proxy.ServeHTTP(w, r) }