package internals import ( "crypto/tls" "flag" "fmt" "log" "net" "net/http" "os" "path/filepath" "strings" "time" qrcode "github.com/skip2/go-qrcode" ) // Run is the application entry point, called from main(). func Run() { addr := flag.String("addr", "127.0.0.1:5000", "listen address") nopw := flag.Bool("nopw", false, "disable password authentication") setlogin := flag.String("setlogin", "", "set login username (next arg is password)") certFlag := flag.String("cert", "", "set custom TLS certificate PEM file (stored encrypted)") cetkeyFlag := flag.String("certkey", "", "set custom TLS private key PEM file") certreset := flag.Bool("certreset", false, "remove stored custom certificate, revert to self-signed") logFlag := flag.String("log", "", "auth log file path; 'off' disables file logging (default: gotermix.log next to binary)") mfaFlag := flag.String("mfa", "", "manage MFA for a user: -mfa on|off") flag.Parse() initialCwd, _ = os.Getwd() // Credentials file lives next to the executable. exe, err := os.Executable() if err != nil { exe = "." } credsPath = filepath.Join(filepath.Dir(exe), credsFilename) // ── -certreset: remove stored cert paths ───────────────────── if *certreset { c := loadCreds() c.CertFile = "" c.KeyFile = "" if err := saveCreds(c); err != nil { fmt.Fprintf(os.Stderr, "error saving config: %v\n", err) os.Exit(1) } fmt.Println("custom certificate removed — will use self-signed on next start") os.Exit(0) } // ── -cert / -certkey: store custom cert paths (encrypted) ───── if *certFlag != "" { keyPath := *cetkeyFlag if keyPath == "" { keyPath = *certFlag // allow combined cert+key PEM } // Validate the files are readable and form a valid keypair before storing. if _, err := tls.LoadX509KeyPair(*certFlag, keyPath); err != nil { fmt.Fprintf(os.Stderr, "certificate error: %v\n", err) os.Exit(1) } c := loadCreds() c.CertFile = *certFlag c.KeyFile = keyPath if err := saveCreds(c); err != nil { fmt.Fprintf(os.Stderr, "error saving config: %v\n", err) os.Exit(1) } fmt.Println("custom certificate stored") os.Exit(0) } // ── -setlogin username password ──────────────────────────────── if *setlogin != "" { args := flag.Args() if len(args) < 1 { fmt.Fprintln(os.Stderr, "usage: -setlogin ") os.Exit(1) } existing := loadCreds() // preserve cert paths across credential changes salt := newSalt() c := storedCreds{ Username: *setlogin, Salt: salt, Hash: hashPassword(args[0], salt), CertFile: existing.CertFile, KeyFile: existing.KeyFile, } if err := saveCreds(c); err != nil { fmt.Fprintf(os.Stderr, "error saving credentials: %v\n", err) os.Exit(1) } fmt.Printf("credentials saved user=%q file=%s\n", *setlogin, credsPath) os.Exit(0) } // ── -mfa on|off ──────────────────────────────────────── if *mfaFlag != "" { args := flag.Args() if len(args) < 1 || (args[0] != "on" && args[0] != "off") { fmt.Fprintln(os.Stderr, "usage: -mfa on|off") os.Exit(1) } c := loadCreds() if !strings.EqualFold(c.Username, *mfaFlag) { fmt.Fprintf(os.Stderr, "unknown user %q (current user is %q)\n", *mfaFlag, c.Username) os.Exit(1) } if args[0] == "off" { c.MFAEnabled = false c.MFASecret = "" if err := saveCreds(c); err != nil { fmt.Fprintf(os.Stderr, "error saving credentials: %v\n", err) os.Exit(1) } fmt.Printf("MFA disabled for user %q\n", c.Username) os.Exit(0) } // on: generate new secret secret := newTOTPSecret() c.MFASecret = secret c.MFAEnabled = true if err := saveCreds(c); err != nil { fmt.Fprintf(os.Stderr, "error saving credentials: %v\n", err) os.Exit(1) } otpauthURL := totpOtpauthURL(secret, c.Username, "GoTermix") fmt.Printf("MFA enabled for user %q\n\n", c.Username) fmt.Printf("Secret (manual entry): %s\n\n", secret) fmt.Printf("otpauth URL: %s\n\n", otpauthURL) fmt.Println("Scan with your authenticator app (Google Authenticator, Aegis, Authy ...):") fmt.Println() qr, err := qrcode.New(otpauthURL, qrcode.Medium) if err != nil { fmt.Fprintf(os.Stderr, "QR error: %v\n", err) } else { fmt.Println(qr.ToSmallString(false)) } fmt.Println("If the QR code does not render, enter the secret manually in your app.") os.Exit(0) } nopwMode = *nopw appCreds = loadCreds() initAuthSecret() if nopwMode { fmt.Println("auth: DISABLED (-nopw)") } else { fmt.Printf("auth: enabled user=%q creds=%s\n", appCreds.Username, credsPath) } // Auth logging — default path is gotermix.log next to the binary. logPath := *logFlag if logPath == "" { logPath = filepath.Join(filepath.Dir(exe), "gotermix.log") } initAuthLogger(logPath) // Reap idle sessions. go func() { t := time.NewTicker(10 * time.Minute) defer t.Stop() for range t.C { sessionsMu.Lock() for id, s := range sessions { s.mu.Lock() idle := time.Since(s.lastSeen) s.mu.Unlock() if idle > sessionTTL { s.ptty.Close() delete(sessions, id) } } sessionsMu.Unlock() } }() // Load TLS certificate — custom (stored encrypted) or auto-generated. // The cert path is never printed to avoid leaking it in logs or terminal history. var tlsCert tls.Certificate if appCreds.CertFile != "" && appCreds.KeyFile != "" { if cert, err := tls.LoadX509KeyPair(appCreds.CertFile, appCreds.KeyFile); err == nil { tlsCert = cert fmt.Println("TLS: custom certificate") } else { fmt.Fprintln(os.Stderr, "TLS: custom certificate failed to load, falling back to self-signed") tlsCert, _ = generateSelfSignedCert() } } else { tlsCert, _ = generateSelfSignedCert() fmt.Println("TLS: self-signed (auto-generated)") } mux := http.NewServeMux() mux.HandleFunc("/", handleIndex) mux.HandleFunc("/login", handleLogin) mux.HandleFunc("/s/", handleShell) mux.HandleFunc("/ws/", handleWS) mux.HandleFunc("/auth", handleAuth) mux.HandleFunc("/upload", handleUpload) mux.HandleFunc("/download", handleDownload) mux.HandleFunc("/favicon.svg", handleFavicon) mux.HandleFunc("/static/app.css", handleStaticCSS) mux.HandleFunc("/static/app.js", handleStaticJS) mux.HandleFunc("/api/workspace/", handleWorkspaceAPI) ln, _ := net.Listen("tcp", *addr) fmt.Printf("Go Web Shell https://%s\n", *addr) fmt.Println(" / new session (URL stays clean)") fmt.Println(" /s/ reconnect to a specific session") // Suppress the noisy "TLS handshake error" lines that appear whenever a // browser rejects the self-signed certificate during the initial TLS dance. srv := &http.Server{ Handler: mux, ErrorLog: log.New(tlsHandshakeFilter{log.Writer()}, "", log.LstdFlags), } srv.Serve(tls.NewListener(ln, &tls.Config{Certificates: []tls.Certificate{tlsCert}})) //nolint:errcheck }