server.go 4.6 KB

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