package services import ( "encoding/json" "fmt" "os" "path/filepath" "strings" "sync" "time" "proxmox-router/internal/logging" ) const ( DefaultServicesPath = "/etc/proxmox-router/services.json" ServiceTelnet = "telnet" ServiceSSH = "ssh" ServiceWWW = "www" ) type Service struct { Name string `json:"name"` // "telnet", "ssh", "www" Port int `json:"port"` // Port number Disabled bool `json:"disabled"` // Active status Comment string `json:"comment,omitempty"` // Description } type ServiceHook func(serviceName string, port int, disabled bool) error type Manager struct { mu sync.RWMutex filePath string services map[string]Service hooks map[string]ServiceHook } func NewManager(customPath string) *Manager { p := customPath if p == "" { p = DefaultServicesPath } m := &Manager{ filePath: p, services: make(map[string]Service), hooks: make(map[string]ServiceHook), } m.initDefaults() _ = m.load() return m } func (m *Manager) initDefaults() { m.services[ServiceWWW] = Service{ Name: ServiceWWW, Port: 80, Disabled: false, Comment: "HTTP Web Management & REST API", } m.services[ServiceTelnet] = Service{ Name: ServiceTelnet, Port: 8282, Disabled: false, Comment: "RouterOS CLI Console over TCP/Telnet", } m.services[ServiceSSH] = Service{ Name: ServiceSSH, Port: 2222, Disabled: false, Comment: "RouterOS CLI Secure Shell (SSH)", } } func (m *Manager) RegisterHook(serviceName string, hook ServiceHook) { m.mu.Lock() defer m.mu.Unlock() m.hooks[strings.ToLower(serviceName)] = hook } func (m *Manager) load() error { data, err := os.ReadFile(m.filePath) if err != nil { if os.IsNotExist(err) { return m.saveLocked() } return err } var list []Service if err := json.Unmarshal(data, &list); err != nil { return err } for _, s := range list { name := strings.ToLower(s.Name) if cur, ok := m.services[name]; ok { cur.Port = s.Port cur.Disabled = s.Disabled if s.Comment != "" { cur.Comment = s.Comment } m.services[name] = cur } else { m.services[name] = s } } return nil } func (m *Manager) saveLocked() error { dir := filepath.Dir(m.filePath) if err := os.MkdirAll(dir, 0755); err != nil { return err } list := make([]Service, 0, len(m.services)) // Keep ordered: www, telnet, ssh order := []string{ServiceWWW, ServiceTelnet, ServiceSSH} for _, name := range order { if s, ok := m.services[name]; ok { list = append(list, s) } } for name, s := range m.services { if name != ServiceWWW && name != ServiceTelnet && name != ServiceSSH { list = append(list, s) } } data, err := json.MarshalIndent(list, "", " ") if err != nil { return err } tmp := filepath.Join(dir, fmt.Sprintf(".services.%d.tmp", time.Now().UnixNano())) if err := os.WriteFile(tmp, data, 0644); err != nil { return err } return os.Rename(tmp, m.filePath) } func (m *Manager) ListServices() []Service { m.mu.RLock() defer m.mu.RUnlock() var list []Service order := []string{ServiceWWW, ServiceTelnet, ServiceSSH} for _, name := range order { if s, ok := m.services[name]; ok { list = append(list, s) } } return list } func normalizeServiceName(name string) string { k := strings.ToLower(strings.TrimSpace(name)) if k == "web" { return ServiceWWW } return k } func (m *Manager) GetService(name string) (Service, bool) { m.mu.RLock() defer m.mu.RUnlock() s, ok := m.services[normalizeServiceName(name)] return s, ok } func (m *Manager) SetServiceState(name string, disabled bool) error { m.mu.Lock() defer m.mu.Unlock() key := normalizeServiceName(name) svc, ok := m.services[key] if !ok { return fmt.Errorf("service %q not found", name) } svc.Disabled = disabled m.services[key] = svc if err := m.saveLocked(); err != nil { return err } logging.Log("system,info", "Service %s state changed: disabled=%t", key, disabled) // Trigger hook if registered if hook, exists := m.hooks[key]; exists { go func() { _ = hook(key, svc.Port, svc.Disabled) }() } return nil } func (m *Manager) SetService(name string, port int, disabled bool) error { m.mu.Lock() defer m.mu.Unlock() key := normalizeServiceName(name) svc, ok := m.services[key] if !ok { return fmt.Errorf("service %q not found", name) } if port > 0 && port < 65536 { svc.Port = port } svc.Disabled = disabled m.services[key] = svc if err := m.saveLocked(); err != nil { return err } logging.Log("system,info", "Service %s updated: port=%d, disabled=%t", key, svc.Port, svc.Disabled) if hook, exists := m.hooks[key]; exists { go func() { _ = hook(key, svc.Port, svc.Disabled) }() } return nil }