Project Initialisation
added Project files
This commit is contained in:
254
cmd/pulsegate-gui/main.go
Normal file
254
cmd/pulsegate-gui/main.go
Normal file
@@ -0,0 +1,254 @@
|
||||
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"
|
||||
}
|
||||
Reference in New Issue
Block a user