manager.go 6.6 KB

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