package main import ( "fmt" "io" "log" "net" "os" "os/exec" "path/filepath" "strings" "time" "github.com/charmbracelet/huh" "github.com/spf13/cobra" "github.com/spf13/viper" "golang.org/x/crypto/ssh" ) type Config struct { SSHUser string `mapstructure:"SSH_USER"` SSHHost string `mapstructure:"SSH_HOST"` JumpUser string `mapstructure:"JUMP_USER"` JumpHost string `mapstructure:"JUMP_HOST"` WorkstationHost string `mapstructure:"WORKSTATION_HOST"` WorkstationUser string `mapstructure:"WORKSTATION_USER"` WorkstationMac string `mapstructure:"WORKSTATION_MAC"` RDPUser string `mapstructure:"RDP_USER"` RDPPassword string `mapstructure:"RDP_PASSWORD"` WorkstationIP string `mapstructure:"WORKSTATION_IP"` SSHPort int `mapstructure:"SSH_PORT"` } type Flags struct { ShowWeek bool ShowMonth bool ShowExport bool } type App struct { cfg Config flags Flags } func NewApp() (*App, error) { cfg, err := loadConfig() if err != nil { return nil, fmt.Errorf("error loading config: %w", err) } return &App{ cfg: cfg, }, nil } func loadConfig() (Config, error) { var cfg Config configPath, err := os.UserConfigDir() if err != nil { return cfg, err } workConfigPath := filepath.Join(configPath, "work") configFile := filepath.Join(workConfigPath, "config.toml") viper.SetConfigFile(configFile) viper.SetConfigType("toml") viper.AddConfigPath(".") viper.AutomaticEnv() if err := viper.ReadInConfig(); err != nil { return cfg, fmt.Errorf("error reading config file: %w", err) } if err := viper.UnmarshalKey("default", &cfg); err != nil { return cfg, fmt.Errorf("error decoding config: %w", err) } return cfg, nil } func (a *App) connect() { tw := NewTimeWarrior() tw.StartWork() // a.runCommand("timew", "start", "work") a.wakeWorkstation() sshClient, err := ssh.Dial("tcp", a.cfg.SSHHost+":"+fmt.Sprintf("%v", a.cfg.SSHPort), a.makeSSHClient()) if err != nil { log.Fatal("Failed to dial: ", err) } defer sshClient.Close() session, err := sshClient.NewSession() if err != nil { log.Fatal("Failed to create session: ", err) } defer session.Close() go forwardPort(sshClient, "2048", a.cfg.WorkstationIP, "22") go forwardPort(sshClient, "6000", a.cfg.WorkstationIP, "3389") a.connectToWorkstation() } func forwardPort(sshConn *ssh.Client, localPort, remoteHost, remotePort string) { listener, err := net.Listen("tcp", "127.0.0.1:"+localPort) if err != nil { log.Printf("Fehler beim Öffnen des lokalen Ports %s: %v", localPort, err) return } defer listener.Close() for { localConn, err := listener.Accept() if err != nil { log.Printf("Fehler beim Akzeptieren der Verbindung: %v", err) continue } remoteConn, err := sshConn.Dial("tcp", remoteHost+":"+remotePort) if err != nil { log.Printf("Fehler beim Verbinden zum Remote-Host %s:%s: %v", remoteHost, remotePort, err) localConn.Close() continue } go copyConn(localConn, remoteConn) go copyConn(remoteConn, localConn) } } func copyConn(dst, src net.Conn) { defer dst.Close() defer src.Close() io.Copy(dst, src) } func (a *App) makeSSHClient() *ssh.ClientConfig { keypath := os.ExpandEnv("$HOME/.ssh/hegenberg") keyBytes, err := os.ReadFile(keypath) if err != nil { log.Fatalf("Failed to read private key: %s", err) } key, err := ssh.ParsePrivateKeyWithPassphrase(keyBytes, []byte(a.cfg.RDPPassword)) if err != nil { log.Fatalf("Failed to parse private key: %s", err) } return &ssh.ClientConfig{ User: a.cfg.SSHUser, Auth: []ssh.AuthMethod{ ssh.PublicKeys(key), }, HostKeyCallback: ssh.InsecureIgnoreHostKey(), Timeout: 5 * time.Second, } } func (a *App) runCommand(name string, args ...string) { cmd := exec.Command(name, args...) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr cmd.Stdin = os.Stdin err := cmd.Run() if err != nil { fmt.Println("Error:", err) } } func (a *App) wakeWorkstation() { sshCommand := fmt.Sprintf("ssh -tt -p %s %s@%s ssh -tt %s@%s \"wakeonlan %s && exit\"", fmt.Sprintf("%v", a.cfg.SSHPort), a.cfg.SSHUser, a.cfg.SSHHost, a.cfg.JumpUser, a.cfg.JumpHost, a.cfg.WorkstationMac) args := strings.Split(sshCommand, " ") log.Println(args) a.runCommand("ssh", args[1:]...) } func (a *App) connectToJump() { sshCommand := fmt.Sprintf("ssh -tt -L 2048:%s:22 %s@%s", a.cfg.WorkstationHost, a.cfg.SSHUser, a.cfg.SSHHost) args := strings.Split(sshCommand, " ") a.runCommand("ssh", args[1:]...) } func (a *App) connectToWorkstation() { sshCommand := fmt.Sprintf("ssh -tt -L 6000:%s:3389 -p 2048 %s@127.0.0.1", a.cfg.WorkstationHost, a.cfg.SSHUser) args := strings.Split(sshCommand, " ") a.runCommand("ssh", args[1:]...) } func (a *App) startRDPConnection() { rdpCommand := fmt.Sprintf("xfreerdp /u:%s /p:%s /v:127.0.0.1:6000 /size:3000x1350", a.cfg.RDPUser, a.cfg.RDPPassword) a.runCommand("bash", "-c", rdpCommand) } func (a *App) makeChoice() { var choice string tw := NewTimeWarrior() form := huh.NewForm( huh.NewGroup( huh.NewSelect[string](). Title("What would you like to do?"). Options( huh.NewOption("Start", "Start"), huh.NewOption("Stop Work", "stop Work"), huh.NewOption("Show Week Summary", "show week summary"), huh.NewOption("Show Month Summary", "show month summary"), huh.NewOption("Start Break", "start break"), huh.NewOption("Stop Break", "stop break"), huh.NewOption("Export Timetable", "export"), huh.NewOption("Wake Workstation", "wake workstation"), huh.NewOption("Connect to Jump", "connect to jump"), huh.NewOption("Connect to Workstation", "connect to workstation"), huh.NewOption("Start RDP Connection", "start rdp connection"), ). Value(&choice), ), ) err := form.Run() if err != nil { fmt.Println("Error:", err) return } switch choice { case "Start": a.connect() case "stop Work": tw.StopWork() case "start break": tw.StartBreak() case "stop break": tw.StopBreak() case "show week summary": tw.ShowSummary(":week") case "show month summary": tw.ShowSummary(":month") case "wake workstation": a.wakeWorkstation() case "connect to jump": a.connectToJump() case "connect to workstation": a.connectToWorkstation() case "start rdp connection": a.startRDPConnection() case "export": tw.ExportSummary() } } type TimeWarrior struct{} func NewTimeWarrior() *TimeWarrior { return &TimeWarrior{} } func (t *TimeWarrior) runCommand(args ...string) error { cmd := exec.Command("timew", args...) cmd.Stdout = os.Stdout cmd.Stdin = os.Stdin cmd.Stderr = os.Stderr return cmd.Run() } func (t *TimeWarrior) StartWork() error { return t.runCommand("start", "work") } func (t *TimeWarrior) StopWork() error { return t.runCommand("stop", "work") } func (t *TimeWarrior) StartBreak() error { return t.runCommand("track", "break") } func (t *TimeWarrior) StopBreak() error { return t.runCommand("track", "work") } func (t *TimeWarrior) ShowSummary(period string) error { return t.runCommand("summary", period, "work") } func (t *TimeWarrior) ExportSummary() error { fmt.Println("Export Timetable") return nil } func (a *App) setupCommands() *cobra.Command { rootCmd := &cobra.Command{ Use: "work", Short: "Fast work interactions", Long: `A CLI tool to perform basic work cli tasks.`, } rootCmd.AddCommand(a.startCommand()) rootCmd.AddCommand(a.stopCommand()) rootCmd.AddCommand(a.showCommand()) return rootCmd } func (a *App) startCommand() *cobra.Command { return &cobra.Command{ Use: "start", Short: "start work", Long: "command to start the work day", Run: func(cmd *cobra.Command, args []string) { a.connect() }, } } func (a *App) stopCommand() *cobra.Command { return &cobra.Command{ Use: "stop", Short: "stop work", Long: "command to stop the work day", Run: func(cmd *cobra.Command, args []string) { a.runCommand("timew", "stop", "work") os.Exit(0) }, } } func (a *App) showCommand() *cobra.Command { cmd := &cobra.Command{ Use: "show", Short: "show timetracking", Long: "show different timetracking", Run: func(cmd *cobra.Command, args []string) { if a.flags.ShowExport { fmt.Println("Exporting timetable") } if a.flags.ShowWeek { a.runCommand("timew", "summary", ":week", "work") } if a.flags.ShowMonth { a.runCommand("timew", "summary", ":month", "work") } os.Exit(0) }, } cmd.Flags().BoolVarP(&a.flags.ShowWeek, "week", "w", false, "show timewarrior week summary") cmd.Flags().BoolVarP(&a.flags.ShowMonth, "month", "m", false, "show timewarrior month summary") cmd.Flags().BoolVarP(&a.flags.ShowExport, "export", "e", false, "export timewarrior timetable") return cmd } func main() { app, err := NewApp() if err != nil { log.Fatalf("unable to setup application: %v", err) } if len(os.Args) > 1 { if err := app.setupCommands().Execute(); err != nil { log.Fatalf("error executing command: %v", err) } } else { app.makeChoice() } }