| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234 |
- package cli
- import (
- "errors"
- "fmt"
- "io"
- "log"
- "net"
- "strings"
- "sync"
- "golang.org/x/crypto/ssh/terminal"
- "debtik/internal/auth"
- "debtik/internal/system"
- )
- type Server struct {
- addr string
- authMgr *auth.Manager
- executor *Executor
- listener net.Listener
- mu sync.Mutex
- stopChan chan struct{}
- running bool
- }
- func NewServer(
- addr string,
- authMgr *auth.Manager,
- executor *Executor,
- ) *Server {
- if addr == "" {
- addr = ":8282"
- }
- return &Server{
- addr: addr,
- authMgr: authMgr,
- executor: executor,
- stopChan: make(chan struct{}),
- }
- }
- func (s *Server) SetAddr(addr string) {
- s.mu.Lock()
- defer s.mu.Unlock()
- s.addr = addr
- }
- func (s *Server) IsRunning() bool {
- s.mu.Lock()
- defer s.mu.Unlock()
- return s.running
- }
- func (s *Server) Start() error {
- s.mu.Lock()
- if s.running {
- s.mu.Unlock()
- return nil
- }
- s.stopChan = make(chan struct{})
- l, err := net.Listen("tcp", s.addr)
- if err != nil {
- s.mu.Unlock()
- return fmt.Errorf("failed to bind CLI server on %s: %w", s.addr, err)
- }
- s.listener = l
- s.running = true
- stopCh := s.stopChan
- s.mu.Unlock()
- log.Printf("[CLI] DebTik CLI server listening on %s (TCP/Telnet)", s.addr)
- for {
- conn, err := l.Accept()
- if err != nil {
- select {
- case <-stopCh:
- return nil
- default:
- continue
- }
- }
- go s.handleConn(conn)
- }
- }
- func (s *Server) Stop() {
- s.mu.Lock()
- defer s.mu.Unlock()
- if !s.running {
- return
- }
- s.running = false
- close(s.stopChan)
- if s.listener != nil {
- _ = s.listener.Close()
- }
- log.Printf("[CLI] Telnet CLI server stopped")
- }
- func (s *Server) handleConn(rawConn net.Conn) {
- defer rawConn.Close()
- telnet := NewTelnetConn(rawConn)
- term := terminal.NewTerminal(telnet, "")
- term.SetSize(120, 40)
- // Authentication Loop (up to 3 attempts)
- var authenticatedUser *auth.PublicUser
- for attempt := 1; attempt <= 3; attempt++ {
- term.SetPrompt("DebTik login: ")
- username, err := term.ReadLine()
- if err != nil {
- return
- }
- username = strings.TrimSpace(username)
- password, err := term.ReadPassword("Password: ")
- if err != nil {
- return
- }
- if username == "" {
- continue
- }
- sess, authErr := s.authMgr.Authenticate(username, password)
- if authErr == nil && sess != nil {
- pub, getErr := s.authMgr.GetPublicUser(username)
- if getErr == nil && pub != nil {
- authenticatedUser = pub
- break
- }
- }
- if errors.Is(authErr, auth.ErrUserDisabled) {
- _, _ = term.Write([]byte(ColorRed + "Login failed: user account is disabled\n\n" + ColorReset))
- return
- }
- _, _ = term.Write([]byte(ColorRed + "Login failed: invalid username or password\n\n" + ColorReset))
- }
- if authenticatedUser == nil {
- _, _ = term.Write([]byte(ColorRed + "Too many failed login attempts. Disconnecting.\n" + ColorReset))
- return
- }
- // Successful login: display welcome banner
- st, _ := system.GetStatus()
- hostname := "DebTik"
- if st != nil && st.Hostname != "" {
- hostname = st.Hostname
- }
- banner := Banner("1.0", authenticatedUser.Username, string(authenticatedUser.Role), hostname)
- _, _ = term.Write([]byte(banner))
- // Interactive Shell Prompt
- prompt := fmt.Sprintf("[%s%s%s@%s%s%s] > ", ColorGreen+ColorBold, authenticatedUser.Username, ColorReset, ColorCyan+ColorBold, hostname, ColorReset)
- term.SetPrompt(prompt)
- completer := NewCompleter(term, s.executor)
- term.AutoCompleteCallback = completer.Callback
- for {
- line, err := term.ReadLine()
- if err != nil {
- if err == io.EOF {
- break
- }
- return
- }
- trimmed := strings.TrimSpace(line)
- if trimmed == "" {
- continue
- }
- // Special self password change command
- if trimmed == "password" {
- s.handleInteractivePasswordChange(term, authenticatedUser)
- continue
- }
- output, shouldExit := s.executor.Execute(trimmed, authenticatedUser)
- if output != "" {
- _, _ = term.Write([]byte(output))
- }
- if shouldExit {
- break
- }
- }
- }
- func (s *Server) handleInteractivePasswordChange(term *terminal.Terminal, user *auth.PublicUser) {
- currPass, err := term.ReadPassword("Current password: ")
- if err != nil {
- return
- }
- newPass, err := term.ReadPassword("New password: ")
- if err != nil {
- return
- }
- if len(newPass) < 4 {
- _, _ = term.Write([]byte(ColorRed + "Password must be at least 4 characters\n" + ColorReset))
- return
- }
- confirmPass, err := term.ReadPassword("Confirm new password: ")
- if err != nil {
- return
- }
- if newPass != confirmPass {
- _, _ = term.Write([]byte(ColorRed + "Passwords do not match\n" + ColorReset))
- return
- }
- if err := s.authMgr.ChangePassword(user.Username, currPass, newPass, false); err != nil {
- _, _ = term.Write([]byte(ColorRed + "Error: " + err.Error() + "\n" + ColorReset))
- return
- }
- _, _ = term.Write([]byte(ColorGreen + "Password changed successfully!\n" + ColorReset))
- }
|