manager.go 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279
  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. ServiceWWWSSL = "www-ssl"
  18. )
  19. type Service struct {
  20. Name string `json:"name"` // "telnet", "ssh", "www", "www-ssl"
  21. Port int `json:"port"` // Port number
  22. Disabled bool `json:"disabled"` // Active status
  23. Certificate string `json:"certificate,omitempty"` // TLS certificate name for www-ssl
  24. Comment string `json:"comment,omitempty"` // Description
  25. }
  26. type ServiceHook func(serviceName string, port int, disabled bool) error
  27. type Manager struct {
  28. mu sync.RWMutex
  29. filePath string
  30. services map[string]Service
  31. hooks map[string]ServiceHook
  32. }
  33. func NewManager(customPath string) *Manager {
  34. p := customPath
  35. if p == "" {
  36. p = DefaultServicesPath
  37. }
  38. m := &Manager{
  39. filePath: p,
  40. services: make(map[string]Service),
  41. hooks: make(map[string]ServiceHook),
  42. }
  43. m.initDefaults()
  44. _ = m.load()
  45. return m
  46. }
  47. func (m *Manager) initDefaults() {
  48. m.services[ServiceWWW] = Service{
  49. Name: ServiceWWW,
  50. Port: 80,
  51. Disabled: false,
  52. Comment: "HTTP Web Management & REST API",
  53. }
  54. m.services[ServiceWWWSSL] = Service{
  55. Name: ServiceWWWSSL,
  56. Port: 443,
  57. Disabled: true,
  58. Certificate: "",
  59. Comment: "HTTPS Secure Web Management & REST API",
  60. }
  61. m.services[ServiceTelnet] = Service{
  62. Name: ServiceTelnet,
  63. Port: 8282,
  64. Disabled: false,
  65. Comment: "RouterOS CLI Console over TCP/Telnet",
  66. }
  67. m.services[ServiceSSH] = Service{
  68. Name: ServiceSSH,
  69. Port: 2222,
  70. Disabled: false,
  71. Comment: "RouterOS CLI Secure Shell (SSH)",
  72. }
  73. }
  74. func (m *Manager) RegisterHook(serviceName string, hook ServiceHook) {
  75. m.mu.Lock()
  76. defer m.mu.Unlock()
  77. m.hooks[strings.ToLower(serviceName)] = hook
  78. }
  79. func (m *Manager) load() error {
  80. data, err := os.ReadFile(m.filePath)
  81. if err != nil {
  82. if os.IsNotExist(err) {
  83. return m.saveLocked()
  84. }
  85. return err
  86. }
  87. var list []Service
  88. if err := json.Unmarshal(data, &list); err != nil {
  89. return err
  90. }
  91. for _, s := range list {
  92. name := normalizeServiceName(s.Name)
  93. if cur, ok := m.services[name]; ok {
  94. cur.Port = s.Port
  95. cur.Disabled = s.Disabled
  96. if s.Certificate != "" {
  97. cur.Certificate = s.Certificate
  98. }
  99. if s.Comment != "" {
  100. cur.Comment = s.Comment
  101. }
  102. m.services[name] = cur
  103. } else {
  104. m.services[name] = s
  105. }
  106. }
  107. return nil
  108. }
  109. func (m *Manager) saveLocked() error {
  110. dir := filepath.Dir(m.filePath)
  111. if err := os.MkdirAll(dir, 0755); err != nil {
  112. return err
  113. }
  114. list := make([]Service, 0, len(m.services))
  115. // Keep ordered: www, www-ssl, telnet, ssh
  116. order := []string{ServiceWWW, ServiceWWWSSL, ServiceTelnet, ServiceSSH}
  117. for _, name := range order {
  118. if s, ok := m.services[name]; ok {
  119. list = append(list, s)
  120. }
  121. }
  122. for name, s := range m.services {
  123. if name != ServiceWWW && name != ServiceWWWSSL && name != ServiceTelnet && name != ServiceSSH {
  124. list = append(list, s)
  125. }
  126. }
  127. data, err := json.MarshalIndent(list, "", " ")
  128. if err != nil {
  129. return err
  130. }
  131. tmp := filepath.Join(dir, fmt.Sprintf(".services.%d.tmp", time.Now().UnixNano()))
  132. if err := os.WriteFile(tmp, data, 0644); err != nil {
  133. return err
  134. }
  135. return os.Rename(tmp, m.filePath)
  136. }
  137. func (m *Manager) ListServices() []Service {
  138. m.mu.RLock()
  139. defer m.mu.RUnlock()
  140. var list []Service
  141. order := []string{ServiceWWW, ServiceWWWSSL, ServiceTelnet, ServiceSSH}
  142. for _, name := range order {
  143. if s, ok := m.services[name]; ok {
  144. list = append(list, s)
  145. }
  146. }
  147. return list
  148. }
  149. func normalizeServiceName(name string) string {
  150. k := strings.ToLower(strings.TrimSpace(name))
  151. if k == "web" {
  152. return ServiceWWW
  153. }
  154. if k == "https" || k == "ssl" || k == "web-ssl" || k == "www-ssl" {
  155. return ServiceWWWSSL
  156. }
  157. return k
  158. }
  159. func (m *Manager) GetService(name string) (Service, bool) {
  160. m.mu.RLock()
  161. defer m.mu.RUnlock()
  162. s, ok := m.services[normalizeServiceName(name)]
  163. return s, ok
  164. }
  165. func (m *Manager) SetServiceState(name string, disabled bool) error {
  166. m.mu.Lock()
  167. defer m.mu.Unlock()
  168. key := normalizeServiceName(name)
  169. svc, ok := m.services[key]
  170. if !ok {
  171. return fmt.Errorf("service %q not found", name)
  172. }
  173. svc.Disabled = disabled
  174. m.services[key] = svc
  175. if err := m.saveLocked(); err != nil {
  176. return err
  177. }
  178. logging.Log("system,info", "Service %s state changed: disabled=%t", key, disabled)
  179. // Trigger hook if registered
  180. if hook, exists := m.hooks[key]; exists {
  181. go func() {
  182. _ = hook(key, svc.Port, svc.Disabled)
  183. }()
  184. }
  185. return nil
  186. }
  187. func (m *Manager) SetService(name string, port int, disabled bool, certificate ...string) error {
  188. m.mu.Lock()
  189. defer m.mu.Unlock()
  190. key := normalizeServiceName(name)
  191. svc, ok := m.services[key]
  192. if !ok {
  193. return fmt.Errorf("service %q not found", name)
  194. }
  195. if port > 0 && port < 65536 {
  196. svc.Port = port
  197. }
  198. svc.Disabled = disabled
  199. if len(certificate) > 0 {
  200. svc.Certificate = certificate[0]
  201. }
  202. m.services[key] = svc
  203. if err := m.saveLocked(); err != nil {
  204. return err
  205. }
  206. logging.Log("system,info", "Service %s updated: port=%d, disabled=%t, certificate=%s", key, svc.Port, svc.Disabled, svc.Certificate)
  207. if hook, exists := m.hooks[key]; exists {
  208. go func() {
  209. _ = hook(key, svc.Port, svc.Disabled)
  210. }()
  211. }
  212. return nil
  213. }
  214. func (m *Manager) SetServiceCertificate(name string, certName string) error {
  215. m.mu.Lock()
  216. defer m.mu.Unlock()
  217. key := normalizeServiceName(name)
  218. svc, ok := m.services[key]
  219. if !ok {
  220. return fmt.Errorf("service %q not found", name)
  221. }
  222. svc.Certificate = certName
  223. m.services[key] = svc
  224. if err := m.saveLocked(); err != nil {
  225. return err
  226. }
  227. logging.Log("system,info", "Service %s certificate set to %q", key, certName)
  228. if hook, exists := m.hooks[key]; exists {
  229. go func() {
  230. _ = hook(key, svc.Port, svc.Disabled)
  231. }()
  232. }
  233. return nil
  234. }