package main import ( "encoding/json" "fmt" "log" "net/http" "os" "os/exec" "path/filepath" "strconv" "strings" "pulsegate-gui/internal/config" "pulsegate-gui/internal/models" ) type apiServer struct { configPath string } func main() { configPath := config.DefaultPath() if len(os.Args) > 1 { configPath = os.Args[1] } if err := config.EnsureExists(configPath); err != nil { log.Fatal(err) } app := apiServer{configPath: configPath} mux := http.NewServeMux() mux.HandleFunc("GET /api/config", app.getConfig) mux.HandleFunc("GET /api/capabilities", app.capabilities) mux.HandleFunc("PUT /api/config", app.putConfig) mux.HandleFunc("POST /api/ssh-command/", app.sshCommand) mux.HandleFunc("POST /api/connect/", app.connectSSH) mux.Handle("/", http.FileServer(http.Dir(webDir()))) addr := "127.0.0.1:8090" log.Printf("PulseGate GUI läuft auf http://%s", addr) log.Printf("Config: %s", configPath) if err := http.ListenAndServe(addr, mux); err != nil { log.Fatal(err) } } func (a apiServer) getConfig(w http.ResponseWriter, r *http.Request) { cfg, err := config.Load(a.configPath) if err != nil { writeError(w, http.StatusInternalServerError, err) return } writeJSON(w, http.StatusOK, cfg) } func (a apiServer) putConfig(w http.ResponseWriter, r *http.Request) { var cfg models.AppConfig if err := json.NewDecoder(r.Body).Decode(&cfg); err != nil { writeError(w, http.StatusBadRequest, err) return } if err := config.Save(a.configPath, cfg); err != nil { writeError(w, http.StatusInternalServerError, err) return } writeJSON(w, http.StatusOK, cfg) } func (a apiServer) capabilities(w http.ResponseWriter, r *http.Request) { terminal, ok := detectTerminal() writeJSON(w, http.StatusOK, map[string]any{ "terminal_available": ok, "terminal": terminal, }) } func (a apiServer) sshCommand(w http.ResponseWriter, r *http.Request) { cfg, err := config.Load(a.configPath) if err != nil { writeError(w, http.StatusInternalServerError, err) return } server, err := serverFromRequest(r, cfg, "/api/ssh-command/") if err != nil { writeError(w, http.StatusBadRequest, err) return } writeJSON(w, http.StatusOK, map[string]string{ "command": strings.Join(sshArgs(server), " "), }) } func (a apiServer) connectSSH(w http.ResponseWriter, r *http.Request) { cfg, err := config.Load(a.configPath) if err != nil { writeError(w, http.StatusInternalServerError, err) return } server, err := serverFromRequest(r, cfg, "/api/connect/") if err != nil { writeError(w, http.StatusBadRequest, err) return } command, args, err := terminalCommand(server) if err != nil { writeError(w, http.StatusInternalServerError, err) return } cmd := exec.Command(command, args...) cmd.Env = buildSSHEnv(server, cfg.Settings) if err := cmd.Start(); err != nil { writeError(w, http.StatusInternalServerError, err) return } writeJSON(w, http.StatusOK, map[string]string{ "status": "started", "command": strings.Join(sshArgs(server), " "), }) } func serverFromRequest(r *http.Request, cfg models.AppConfig, prefix string) (models.Server, error) { rawIndex := strings.TrimPrefix(r.URL.Path, prefix) index, err := strconv.Atoi(rawIndex) if err != nil || index < 0 || index >= len(cfg.Servers) { return models.Server{}, fmt.Errorf("ungültiger server index") } return cfg.Servers[index], nil } func sshArgs(server models.Server) []string { args := []string{"ssh", "-p", strconv.Itoa(server.Port)} if server.Key != "" && server.Auth == "key" { args = append(args, "-i", expandHome(server.Key)) } args = append(args, server.User+"@"+server.Host) return args } func terminalCommand(server models.Server) (string, []string, error) { ssh := strings.Join(quoteArgs(sshArgs(server)), " ") title := "PulseGate - " + server.Name holdCommand := ssh + "; printf '\\nSSH beendet. Enter zum Schließen...'; read _" candidates := []struct { name string args []string }{ {"kitty", []string{"--title", title, "sh", "-lc", holdCommand}}, {"konsole", []string{"--new-tab", "-p", "tabtitle=" + title, "-e", "sh", "-lc", holdCommand}}, {"gnome-terminal", []string{"--title", title, "--", "sh", "-lc", holdCommand}}, {"xfce4-terminal", []string{"--title", title, "--command", "sh -lc " + shellQuote(holdCommand)}}, {"alacritty", []string{"--title", title, "-e", "sh", "-lc", holdCommand}}, {"xterm", []string{"-T", title, "-e", "sh", "-lc", holdCommand}}, } for _, candidate := range candidates { if path, err := exec.LookPath(candidate.name); err == nil { return path, candidate.args, nil } } return "", nil, fmt.Errorf("kein unterstützter Terminal-Emulator gefunden") } func detectTerminal() (string, bool) { for _, name := range []string{"kitty", "konsole", "gnome-terminal", "xfce4-terminal", "alacritty", "xterm"} { if path, err := exec.LookPath(name); err == nil { return path, true } } return "", false } func buildSSHEnv(server models.Server, settings models.Settings) []string { env := os.Environ() if settings.Terminal.EnableKittyFix && server.KittyFix { termValue := settings.Terminal.Term if termValue == "" { termValue = "xterm-256color" } env = append(env, "TERM="+termValue) } return env } func expandHome(path string) string { if strings.HasPrefix(path, "~/") { home, _ := os.UserHomeDir() return filepath.Join(home, strings.TrimPrefix(path, "~/")) } return path } func quoteArgs(args []string) []string { quoted := make([]string, len(args)) for i, arg := range args { quoted[i] = shellQuote(arg) } return quoted } func shellQuote(value string) string { if value == "" { return "''" } return "'" + strings.ReplaceAll(value, "'", "'\\''") + "'" } func writeJSON(w http.ResponseWriter, status int, v any) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) _ = json.NewEncoder(w).Encode(v) } func writeError(w http.ResponseWriter, status int, err error) { writeJSON(w, status, map[string]string{"error": err.Error()}) } func webDir() string { candidates := []string{ "web", filepath.Join("..", "..", "web"), } for _, candidate := range candidates { if info, err := os.Stat(candidate); err == nil && info.IsDir() { return candidate } } return "web" }