server.go 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231
  1. package cli
  2. import (
  3. "errors"
  4. "fmt"
  5. "io"
  6. "log"
  7. "net"
  8. "strings"
  9. "sync"
  10. "golang.org/x/crypto/ssh/terminal"
  11. "proxmox-router/internal/auth"
  12. "proxmox-router/internal/system"
  13. )
  14. type Server struct {
  15. addr string
  16. authMgr *auth.Manager
  17. executor *Executor
  18. listener net.Listener
  19. mu sync.Mutex
  20. stopChan chan struct{}
  21. running bool
  22. }
  23. func NewServer(
  24. addr string,
  25. authMgr *auth.Manager,
  26. executor *Executor,
  27. ) *Server {
  28. if addr == "" {
  29. addr = ":8282"
  30. }
  31. return &Server{
  32. addr: addr,
  33. authMgr: authMgr,
  34. executor: executor,
  35. stopChan: make(chan struct{}),
  36. }
  37. }
  38. func (s *Server) SetAddr(addr string) {
  39. s.mu.Lock()
  40. defer s.mu.Unlock()
  41. s.addr = addr
  42. }
  43. func (s *Server) IsRunning() bool {
  44. s.mu.Lock()
  45. defer s.mu.Unlock()
  46. return s.running
  47. }
  48. func (s *Server) Start() error {
  49. s.mu.Lock()
  50. if s.running {
  51. s.mu.Unlock()
  52. return nil
  53. }
  54. s.stopChan = make(chan struct{})
  55. l, err := net.Listen("tcp", s.addr)
  56. if err != nil {
  57. s.mu.Unlock()
  58. return fmt.Errorf("failed to bind CLI server on %s: %w", s.addr, err)
  59. }
  60. s.listener = l
  61. s.running = true
  62. stopCh := s.stopChan
  63. s.mu.Unlock()
  64. log.Printf("[CLI] RouterOS-compatible CLI server listening on %s (TCP/Telnet)", s.addr)
  65. for {
  66. conn, err := l.Accept()
  67. if err != nil {
  68. select {
  69. case <-stopCh:
  70. return nil
  71. default:
  72. continue
  73. }
  74. }
  75. go s.handleConn(conn)
  76. }
  77. }
  78. func (s *Server) Stop() {
  79. s.mu.Lock()
  80. defer s.mu.Unlock()
  81. if !s.running {
  82. return
  83. }
  84. s.running = false
  85. close(s.stopChan)
  86. if s.listener != nil {
  87. _ = s.listener.Close()
  88. }
  89. log.Printf("[CLI] Telnet CLI server stopped")
  90. }
  91. func (s *Server) handleConn(rawConn net.Conn) {
  92. defer rawConn.Close()
  93. telnet := NewTelnetConn(rawConn)
  94. term := terminal.NewTerminal(telnet, "")
  95. term.SetSize(120, 40)
  96. // Authentication Loop (up to 3 attempts)
  97. var authenticatedUser *auth.PublicUser
  98. for attempt := 1; attempt <= 3; attempt++ {
  99. term.SetPrompt("DebTik login: ")
  100. username, err := term.ReadLine()
  101. if err != nil {
  102. return
  103. }
  104. username = strings.TrimSpace(username)
  105. password, err := term.ReadPassword("Password: ")
  106. if err != nil {
  107. return
  108. }
  109. if username == "" {
  110. continue
  111. }
  112. sess, authErr := s.authMgr.Authenticate(username, password)
  113. if authErr == nil && sess != nil {
  114. pub, getErr := s.authMgr.GetPublicUser(username)
  115. if getErr == nil && pub != nil {
  116. authenticatedUser = pub
  117. break
  118. }
  119. }
  120. if errors.Is(authErr, auth.ErrUserDisabled) {
  121. _, _ = term.Write([]byte(ColorRed + "Login failed: user account is disabled\n\n" + ColorReset))
  122. return
  123. }
  124. _, _ = term.Write([]byte(ColorRed + "Login failed: invalid username or password\n\n" + ColorReset))
  125. }
  126. if authenticatedUser == nil {
  127. _, _ = term.Write([]byte(ColorRed + "Too many failed login attempts. Disconnecting.\n" + ColorReset))
  128. return
  129. }
  130. // Successful login: display welcome banner
  131. st, _ := system.GetStatus()
  132. hostname := "DebTik"
  133. if st != nil && st.Hostname != "" {
  134. hostname = st.Hostname
  135. }
  136. banner := Banner("1.0", authenticatedUser.Username, string(authenticatedUser.Role), hostname)
  137. _, _ = term.Write([]byte(banner))
  138. // Interactive Shell Prompt
  139. prompt := fmt.Sprintf("[%s%s%s@%s%s%s] > ", ColorGreen+ColorBold, authenticatedUser.Username, ColorReset, ColorCyan+ColorBold, hostname, ColorReset)
  140. term.SetPrompt(prompt)
  141. for {
  142. line, err := term.ReadLine()
  143. if err != nil {
  144. if err == io.EOF {
  145. break
  146. }
  147. return
  148. }
  149. trimmed := strings.TrimSpace(line)
  150. if trimmed == "" {
  151. continue
  152. }
  153. // Special self password change command
  154. if trimmed == "password" {
  155. s.handleInteractivePasswordChange(term, authenticatedUser)
  156. continue
  157. }
  158. output, shouldExit := s.executor.Execute(trimmed, authenticatedUser)
  159. if output != "" {
  160. _, _ = term.Write([]byte(output))
  161. }
  162. if shouldExit {
  163. break
  164. }
  165. }
  166. }
  167. func (s *Server) handleInteractivePasswordChange(term *terminal.Terminal, user *auth.PublicUser) {
  168. currPass, err := term.ReadPassword("Current password: ")
  169. if err != nil {
  170. return
  171. }
  172. newPass, err := term.ReadPassword("New password: ")
  173. if err != nil {
  174. return
  175. }
  176. if len(newPass) < 4 {
  177. _, _ = term.Write([]byte(ColorRed + "Password must be at least 4 characters\n" + ColorReset))
  178. return
  179. }
  180. confirmPass, err := term.ReadPassword("Confirm new password: ")
  181. if err != nil {
  182. return
  183. }
  184. if newPass != confirmPass {
  185. _, _ = term.Write([]byte(ColorRed + "Passwords do not match\n" + ColorReset))
  186. return
  187. }
  188. if err := s.authMgr.ChangePassword(user.Username, currPass, newPass, false); err != nil {
  189. _, _ = term.Write([]byte(ColorRed + "Error: " + err.Error() + "\n" + ColorReset))
  190. return
  191. }
  192. _, _ = term.Write([]byte(ColorGreen + "Password changed successfully!\n" + ColorReset))
  193. }