Files
drip/internal/server/tunnel/manager.go

186 lines
4.1 KiB
Go

package tunnel
import (
"sync"
"time"
"drip/internal/shared/utils"
"github.com/gorilla/websocket"
"go.uber.org/zap"
)
// Manager manages all active tunnel connections
type Manager struct {
tunnels map[string]*Connection // subdomain -> connection
mu sync.RWMutex
used map[string]bool // track used subdomains
logger *zap.Logger
}
// NewManager creates a new tunnel manager
func NewManager(logger *zap.Logger) *Manager {
return &Manager{
tunnels: make(map[string]*Connection),
used: make(map[string]bool),
logger: logger,
}
}
// Register registers a new tunnel connection
// Returns the assigned subdomain and any error
func (m *Manager) Register(conn *websocket.Conn, customSubdomain string) (string, error) {
m.mu.Lock()
defer m.mu.Unlock()
var subdomain string
if customSubdomain != "" {
// Validate custom subdomain
if !utils.ValidateSubdomain(customSubdomain) {
return "", ErrInvalidSubdomain
}
if utils.IsReserved(customSubdomain) {
return "", ErrReservedSubdomain
}
if m.used[customSubdomain] {
return "", ErrSubdomainTaken
}
subdomain = customSubdomain
} else {
// Generate unique random subdomain
subdomain = m.generateUniqueSubdomain()
}
// Create connection
tc := NewConnection(subdomain, conn, m.logger)
m.tunnels[subdomain] = tc
m.used[subdomain] = true
// Start write pump in background
go tc.StartWritePump()
m.logger.Info("Tunnel registered",
zap.String("subdomain", subdomain),
zap.Int("total_tunnels", len(m.tunnels)),
)
return subdomain, nil
}
// Unregister removes a tunnel connection
func (m *Manager) Unregister(subdomain string) {
m.mu.Lock()
defer m.mu.Unlock()
if tc, ok := m.tunnels[subdomain]; ok {
tc.Close()
delete(m.tunnels, subdomain)
delete(m.used, subdomain)
m.logger.Info("Tunnel unregistered",
zap.String("subdomain", subdomain),
zap.Int("total_tunnels", len(m.tunnels)),
)
}
}
// Get retrieves a tunnel connection by subdomain
func (m *Manager) Get(subdomain string) (*Connection, bool) {
m.mu.RLock()
defer m.mu.RUnlock()
tc, ok := m.tunnels[subdomain]
return tc, ok
}
// List returns all active tunnel connections
func (m *Manager) List() []*Connection {
m.mu.RLock()
defer m.mu.RUnlock()
connections := make([]*Connection, 0, len(m.tunnels))
for _, tc := range m.tunnels {
connections = append(connections, tc)
}
return connections
}
// Count returns the number of active tunnels
func (m *Manager) Count() int {
m.mu.RLock()
defer m.mu.RUnlock()
return len(m.tunnels)
}
// CleanupStale removes stale connections that haven't been active
func (m *Manager) CleanupStale(timeout time.Duration) int {
m.mu.Lock()
defer m.mu.Unlock()
staleSubdomains := []string{}
for subdomain, tc := range m.tunnels {
if !tc.IsAlive(timeout) {
staleSubdomains = append(staleSubdomains, subdomain)
}
}
for _, subdomain := range staleSubdomains {
if tc, ok := m.tunnels[subdomain]; ok {
tc.Close()
delete(m.tunnels, subdomain)
delete(m.used, subdomain)
}
}
if len(staleSubdomains) > 0 {
m.logger.Info("Cleaned up stale tunnels",
zap.Int("count", len(staleSubdomains)),
)
}
return len(staleSubdomains)
}
// StartCleanupTask starts a background task to clean up stale connections
func (m *Manager) StartCleanupTask(interval, timeout time.Duration) {
ticker := time.NewTicker(interval)
go func() {
for range ticker.C {
m.CleanupStale(timeout)
}
}()
}
// generateUniqueSubdomain generates a unique random subdomain
func (m *Manager) generateUniqueSubdomain() string {
const maxAttempts = 10
for i := 0; i < maxAttempts; i++ {
subdomain := utils.GenerateSubdomain(6)
if !m.used[subdomain] && !utils.IsReserved(subdomain) {
return subdomain
}
}
// Fallback: use longer subdomain if collision persists
return utils.GenerateSubdomain(8)
}
// Shutdown gracefully shuts down all tunnels
func (m *Manager) Shutdown() {
m.mu.Lock()
defer m.mu.Unlock()
m.logger.Info("Shutting down tunnel manager",
zap.Int("active_tunnels", len(m.tunnels)),
)
for _, tc := range m.tunnels {
tc.Close()
}
m.tunnels = make(map[string]*Connection)
m.used = make(map[string]bool)
}