manager.go 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225
  1. package services
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. "os"
  6. "path/filepath"
  7. "strings"
  8. "sync"
  9. "time"
  10. "proxmox-router/internal/logging"
  11. )
  12. const (
  13. DefaultServicesPath = "/etc/proxmox-router/services.json"
  14. ServiceTelnet = "telnet"
  15. ServiceSSH = "ssh"
  16. ServiceWWW = "www"
  17. )
  18. type Service struct {
  19. Name string `json:"name"` // "telnet", "ssh", "www"
  20. Port int `json:"port"` // Port number
  21. Disabled bool `json:"disabled"` // Active status
  22. Comment string `json:"comment,omitempty"` // Description
  23. }
  24. type ServiceHook func(serviceName string, port int, disabled bool) error
  25. type Manager struct {
  26. mu sync.RWMutex
  27. filePath string
  28. services map[string]Service
  29. hooks map[string]ServiceHook
  30. }
  31. func NewManager(customPath string) *Manager {
  32. p := customPath
  33. if p == "" {
  34. p = DefaultServicesPath
  35. }
  36. m := &Manager{
  37. filePath: p,
  38. services: make(map[string]Service),
  39. hooks: make(map[string]ServiceHook),
  40. }
  41. m.initDefaults()
  42. _ = m.load()
  43. return m
  44. }
  45. func (m *Manager) initDefaults() {
  46. m.services[ServiceWWW] = Service{
  47. Name: ServiceWWW,
  48. Port: 80,
  49. Disabled: false,
  50. Comment: "HTTP Web Management & REST API",
  51. }
  52. m.services[ServiceTelnet] = Service{
  53. Name: ServiceTelnet,
  54. Port: 8282,
  55. Disabled: false,
  56. Comment: "RouterOS CLI Console over TCP/Telnet",
  57. }
  58. m.services[ServiceSSH] = Service{
  59. Name: ServiceSSH,
  60. Port: 2222,
  61. Disabled: false,
  62. Comment: "RouterOS CLI Secure Shell (SSH)",
  63. }
  64. }
  65. func (m *Manager) RegisterHook(serviceName string, hook ServiceHook) {
  66. m.mu.Lock()
  67. defer m.mu.Unlock()
  68. m.hooks[strings.ToLower(serviceName)] = hook
  69. }
  70. func (m *Manager) load() error {
  71. data, err := os.ReadFile(m.filePath)
  72. if err != nil {
  73. if os.IsNotExist(err) {
  74. return m.saveLocked()
  75. }
  76. return err
  77. }
  78. var list []Service
  79. if err := json.Unmarshal(data, &list); err != nil {
  80. return err
  81. }
  82. for _, s := range list {
  83. name := strings.ToLower(s.Name)
  84. if cur, ok := m.services[name]; ok {
  85. cur.Port = s.Port
  86. cur.Disabled = s.Disabled
  87. if s.Comment != "" {
  88. cur.Comment = s.Comment
  89. }
  90. m.services[name] = cur
  91. } else {
  92. m.services[name] = s
  93. }
  94. }
  95. return nil
  96. }
  97. func (m *Manager) saveLocked() error {
  98. dir := filepath.Dir(m.filePath)
  99. if err := os.MkdirAll(dir, 0755); err != nil {
  100. return err
  101. }
  102. list := make([]Service, 0, len(m.services))
  103. // Keep ordered: www, telnet, ssh
  104. order := []string{ServiceWWW, ServiceTelnet, ServiceSSH}
  105. for _, name := range order {
  106. if s, ok := m.services[name]; ok {
  107. list = append(list, s)
  108. }
  109. }
  110. for name, s := range m.services {
  111. if name != ServiceWWW && name != ServiceTelnet && name != ServiceSSH {
  112. list = append(list, s)
  113. }
  114. }
  115. data, err := json.MarshalIndent(list, "", " ")
  116. if err != nil {
  117. return err
  118. }
  119. tmp := filepath.Join(dir, fmt.Sprintf(".services.%d.tmp", time.Now().UnixNano()))
  120. if err := os.WriteFile(tmp, data, 0644); err != nil {
  121. return err
  122. }
  123. return os.Rename(tmp, m.filePath)
  124. }
  125. func (m *Manager) ListServices() []Service {
  126. m.mu.RLock()
  127. defer m.mu.RUnlock()
  128. var list []Service
  129. order := []string{ServiceWWW, ServiceTelnet, ServiceSSH}
  130. for _, name := range order {
  131. if s, ok := m.services[name]; ok {
  132. list = append(list, s)
  133. }
  134. }
  135. return list
  136. }
  137. func (m *Manager) GetService(name string) (Service, bool) {
  138. m.mu.RLock()
  139. defer m.mu.RUnlock()
  140. s, ok := m.services[strings.ToLower(name)]
  141. return s, ok
  142. }
  143. func (m *Manager) SetServiceState(name string, disabled bool) error {
  144. m.mu.Lock()
  145. defer m.mu.Unlock()
  146. key := strings.ToLower(name)
  147. svc, ok := m.services[key]
  148. if !ok {
  149. return fmt.Errorf("service %q not found", name)
  150. }
  151. svc.Disabled = disabled
  152. m.services[key] = svc
  153. if err := m.saveLocked(); err != nil {
  154. return err
  155. }
  156. logging.Log("system,info", "Service %s state changed: disabled=%t", key, disabled)
  157. // Trigger hook if registered
  158. if hook, exists := m.hooks[key]; exists {
  159. go func() {
  160. _ = hook(key, svc.Port, svc.Disabled)
  161. }()
  162. }
  163. return nil
  164. }
  165. func (m *Manager) SetService(name string, port int, disabled bool) error {
  166. m.mu.Lock()
  167. defer m.mu.Unlock()
  168. key := strings.ToLower(name)
  169. svc, ok := m.services[key]
  170. if !ok {
  171. return fmt.Errorf("service %q not found", name)
  172. }
  173. if port > 0 && port < 65536 {
  174. svc.Port = port
  175. }
  176. svc.Disabled = disabled
  177. m.services[key] = svc
  178. if err := m.saveLocked(); err != nil {
  179. return err
  180. }
  181. logging.Log("system,info", "Service %s updated: port=%d, disabled=%t", key, svc.Port, svc.Disabled)
  182. if hook, exists := m.hooks[key]; exists {
  183. go func() {
  184. _ = hook(key, svc.Port, svc.Disabled)
  185. }()
  186. }
  187. return nil
  188. }