manager.go 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280
  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: "DebTik CLI Console over TCP/Telnet",
  66. }
  67. m.services[ServiceSSH] = Service{
  68. Name: ServiceSSH,
  69. Port: 2222,
  70. Disabled: false,
  71. Comment: "DebTik 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 = strings.ReplaceAll(s.Comment, "RouterOS", "DebTik")
  101. }
  102. m.services[name] = cur
  103. } else {
  104. s.Comment = strings.ReplaceAll(s.Comment, "RouterOS", "DebTik")
  105. m.services[name] = s
  106. }
  107. }
  108. return nil
  109. }
  110. func (m *Manager) saveLocked() error {
  111. dir := filepath.Dir(m.filePath)
  112. if err := os.MkdirAll(dir, 0755); err != nil {
  113. return err
  114. }
  115. list := make([]Service, 0, len(m.services))
  116. // Keep ordered: www, www-ssl, telnet, ssh
  117. order := []string{ServiceWWW, ServiceWWWSSL, ServiceTelnet, ServiceSSH}
  118. for _, name := range order {
  119. if s, ok := m.services[name]; ok {
  120. list = append(list, s)
  121. }
  122. }
  123. for name, s := range m.services {
  124. if name != ServiceWWW && name != ServiceWWWSSL && name != ServiceTelnet && name != ServiceSSH {
  125. list = append(list, s)
  126. }
  127. }
  128. data, err := json.MarshalIndent(list, "", " ")
  129. if err != nil {
  130. return err
  131. }
  132. tmp := filepath.Join(dir, fmt.Sprintf(".services.%d.tmp", time.Now().UnixNano()))
  133. if err := os.WriteFile(tmp, data, 0644); err != nil {
  134. return err
  135. }
  136. return os.Rename(tmp, m.filePath)
  137. }
  138. func (m *Manager) ListServices() []Service {
  139. m.mu.RLock()
  140. defer m.mu.RUnlock()
  141. var list []Service
  142. order := []string{ServiceWWW, ServiceWWWSSL, ServiceTelnet, ServiceSSH}
  143. for _, name := range order {
  144. if s, ok := m.services[name]; ok {
  145. list = append(list, s)
  146. }
  147. }
  148. return list
  149. }
  150. func normalizeServiceName(name string) string {
  151. k := strings.ToLower(strings.TrimSpace(name))
  152. if k == "web" {
  153. return ServiceWWW
  154. }
  155. if k == "https" || k == "ssl" || k == "web-ssl" || k == "www-ssl" {
  156. return ServiceWWWSSL
  157. }
  158. return k
  159. }
  160. func (m *Manager) GetService(name string) (Service, bool) {
  161. m.mu.RLock()
  162. defer m.mu.RUnlock()
  163. s, ok := m.services[normalizeServiceName(name)]
  164. return s, ok
  165. }
  166. func (m *Manager) SetServiceState(name string, disabled bool) error {
  167. m.mu.Lock()
  168. defer m.mu.Unlock()
  169. key := normalizeServiceName(name)
  170. svc, ok := m.services[key]
  171. if !ok {
  172. return fmt.Errorf("service %q not found", name)
  173. }
  174. svc.Disabled = disabled
  175. m.services[key] = svc
  176. if err := m.saveLocked(); err != nil {
  177. return err
  178. }
  179. logging.Log("system,info", "Service %s state changed: disabled=%t", key, disabled)
  180. // Trigger hook if registered
  181. if hook, exists := m.hooks[key]; exists {
  182. go func() {
  183. _ = hook(key, svc.Port, svc.Disabled)
  184. }()
  185. }
  186. return nil
  187. }
  188. func (m *Manager) SetService(name string, port int, disabled bool, certificate ...string) error {
  189. m.mu.Lock()
  190. defer m.mu.Unlock()
  191. key := normalizeServiceName(name)
  192. svc, ok := m.services[key]
  193. if !ok {
  194. return fmt.Errorf("service %q not found", name)
  195. }
  196. if port > 0 && port < 65536 {
  197. svc.Port = port
  198. }
  199. svc.Disabled = disabled
  200. if len(certificate) > 0 {
  201. svc.Certificate = certificate[0]
  202. }
  203. m.services[key] = svc
  204. if err := m.saveLocked(); err != nil {
  205. return err
  206. }
  207. logging.Log("system,info", "Service %s updated: port=%d, disabled=%t, certificate=%s", key, svc.Port, svc.Disabled, svc.Certificate)
  208. if hook, exists := m.hooks[key]; exists {
  209. go func() {
  210. _ = hook(key, svc.Port, svc.Disabled)
  211. }()
  212. }
  213. return nil
  214. }
  215. func (m *Manager) SetServiceCertificate(name string, certName string) error {
  216. m.mu.Lock()
  217. defer m.mu.Unlock()
  218. key := normalizeServiceName(name)
  219. svc, ok := m.services[key]
  220. if !ok {
  221. return fmt.Errorf("service %q not found", name)
  222. }
  223. svc.Certificate = certName
  224. m.services[key] = svc
  225. if err := m.saveLocked(); err != nil {
  226. return err
  227. }
  228. logging.Log("system,info", "Service %s certificate set to %q", key, certName)
  229. if hook, exists := m.hooks[key]; exists {
  230. go func() {
  231. _ = hook(key, svc.Port, svc.Disabled)
  232. }()
  233. }
  234. return nil
  235. }