manager.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574
  1. package reverseproxy
  2. import (
  3. "context"
  4. "crypto/tls"
  5. "encoding/json"
  6. "fmt"
  7. "net"
  8. "net/http"
  9. "net/url"
  10. "os"
  11. "path"
  12. "path/filepath"
  13. "strings"
  14. "sync"
  15. "sync/atomic"
  16. "time"
  17. "debtik/internal/certs"
  18. "debtik/internal/logging"
  19. )
  20. type Manager struct {
  21. filePath string
  22. certMgr *certs.CertManager
  23. mu sync.RWMutex
  24. config Config
  25. handlers map[string]http.Handler
  26. // Standalone listeners
  27. httpsServer *http.Server
  28. httpsMu sync.Mutex
  29. httpsRunning bool
  30. httpServer *http.Server
  31. httpMu sync.Mutex
  32. httpRunning bool
  33. }
  34. func NewManager(customPath string, certMgr *certs.CertManager) *Manager {
  35. p := customPath
  36. if p == "" {
  37. p = DefaultConfigPath
  38. }
  39. m := &Manager{
  40. filePath: p,
  41. certMgr: certMgr,
  42. handlers: make(map[string]http.Handler),
  43. }
  44. m.initDefaults()
  45. _ = m.load()
  46. m.rebuildHandlersLocked()
  47. return m
  48. }
  49. func (m *Manager) initDefaults() {
  50. m.config = Config{
  51. Enabled: true,
  52. HTTPPort: 80,
  53. HTTPSPort: 443,
  54. Rules: []*Rule{},
  55. }
  56. }
  57. func (m *Manager) load() error {
  58. m.mu.Lock()
  59. defer m.mu.Unlock()
  60. data, err := os.ReadFile(m.filePath)
  61. if err != nil {
  62. if os.IsNotExist(err) {
  63. return m.saveLocked()
  64. }
  65. return err
  66. }
  67. var cfg Config
  68. if err := json.Unmarshal(data, &cfg); err != nil {
  69. return err
  70. }
  71. if cfg.HTTPPort == 0 {
  72. cfg.HTTPPort = 80
  73. }
  74. if cfg.HTTPSPort == 0 {
  75. cfg.HTTPSPort = 443
  76. }
  77. if cfg.Rules == nil {
  78. cfg.Rules = []*Rule{}
  79. }
  80. m.config = cfg
  81. return nil
  82. }
  83. func (m *Manager) saveLocked() error {
  84. dir := filepath.Dir(m.filePath)
  85. if err := os.MkdirAll(dir, 0755); err != nil {
  86. return fmt.Errorf("failed to create directory %s: %w", dir, err)
  87. }
  88. data, err := json.MarshalIndent(m.config, "", " ")
  89. if err != nil {
  90. return fmt.Errorf("failed to marshal reverse proxy config: %w", err)
  91. }
  92. tmpFile := fmt.Sprintf("%s.tmp.%d", m.filePath, time.Now().UnixNano())
  93. if err := os.WriteFile(tmpFile, data, 0644); err != nil {
  94. return fmt.Errorf("failed to write tmp file: %w", err)
  95. }
  96. return os.Rename(tmpFile, m.filePath)
  97. }
  98. func (m *Manager) rebuildHandlersLocked() {
  99. m.handlers = make(map[string]http.Handler)
  100. for _, r := range m.config.Rules {
  101. if !r.Enabled {
  102. continue
  103. }
  104. ruleRef := r
  105. h, err := NewProxyHandler(ruleRef, func(reqDelta, rxDelta, txDelta uint64) {
  106. m.mu.Lock()
  107. atomic.AddUint64(&ruleRef.Stats.Requests, reqDelta)
  108. atomic.AddUint64(&ruleRef.Stats.BytesReceived, rxDelta)
  109. atomic.AddUint64(&ruleRef.Stats.BytesSent, txDelta)
  110. ruleRef.Stats.LastAccess = time.Now()
  111. m.mu.Unlock()
  112. })
  113. if err != nil {
  114. logging.Log("proxy,error", "Failed to build proxy handler for rule %s (%s): %v", r.ID, r.Name, err)
  115. continue
  116. }
  117. m.handlers[r.ID] = h
  118. }
  119. }
  120. // IsEnabled returns whether the reverse proxy is globally active.
  121. func (m *Manager) IsEnabled() bool {
  122. m.mu.RLock()
  123. defer m.mu.RUnlock()
  124. return m.config.Enabled
  125. }
  126. // GetConfig returns a deep copy of the global configuration.
  127. func (m *Manager) GetConfig() Config {
  128. m.mu.RLock()
  129. defer m.mu.RUnlock()
  130. cfgCopy := m.config
  131. cfgCopy.Rules = make([]*Rule, len(m.config.Rules))
  132. for i, r := range m.config.Rules {
  133. rc := *r
  134. if r.Domains != nil {
  135. rc.Domains = append([]string(nil), r.Domains...)
  136. }
  137. if r.CustomHeaders != nil {
  138. rc.CustomHeaders = make(map[string]string)
  139. for k, v := range r.CustomHeaders {
  140. rc.CustomHeaders[k] = v
  141. }
  142. }
  143. cfgCopy.Rules[i] = &rc
  144. }
  145. return cfgCopy
  146. }
  147. // UpdateConfig updates the global configuration settings and persists them.
  148. func (m *Manager) UpdateConfig(newCfg Config) error {
  149. m.mu.Lock()
  150. defer m.mu.Unlock()
  151. m.config.Enabled = newCfg.Enabled
  152. if newCfg.HTTPPort > 0 {
  153. m.config.HTTPPort = newCfg.HTTPPort
  154. }
  155. if newCfg.HTTPSPort > 0 {
  156. m.config.HTTPSPort = newCfg.HTTPSPort
  157. }
  158. m.config.DefaultCert = newCfg.DefaultCert
  159. if err := m.saveLocked(); err != nil {
  160. return err
  161. }
  162. m.rebuildHandlersLocked()
  163. return nil
  164. }
  165. // ListRules returns all configured proxy rules.
  166. func (m *Manager) ListRules() []*Rule {
  167. m.mu.RLock()
  168. defer m.mu.RUnlock()
  169. result := make([]*Rule, len(m.config.Rules))
  170. for i, r := range m.config.Rules {
  171. rc := *r
  172. if r.Domains != nil {
  173. rc.Domains = append([]string(nil), r.Domains...)
  174. }
  175. if r.CustomHeaders != nil {
  176. rc.CustomHeaders = make(map[string]string)
  177. for k, v := range r.CustomHeaders {
  178. rc.CustomHeaders[k] = v
  179. }
  180. }
  181. result[i] = &rc
  182. }
  183. return result
  184. }
  185. // GetRule returns a specific rule by ID.
  186. func (m *Manager) GetRule(id string) (*Rule, error) {
  187. m.mu.RLock()
  188. defer m.mu.RUnlock()
  189. for _, r := range m.config.Rules {
  190. if r.ID == id {
  191. rc := *r
  192. if r.Domains != nil {
  193. rc.Domains = append([]string(nil), r.Domains...)
  194. }
  195. return &rc, nil
  196. }
  197. }
  198. return nil, fmt.Errorf("rule %q not found", id)
  199. }
  200. // SaveRule creates or updates a reverse proxy rule.
  201. func (m *Manager) SaveRule(r *Rule) (*Rule, error) {
  202. if r == nil {
  203. return nil, fmt.Errorf("rule cannot be nil")
  204. }
  205. if strings.TrimSpace(r.Name) == "" {
  206. return nil, fmt.Errorf("rule name is required")
  207. }
  208. if strings.TrimSpace(r.TargetURL) == "" {
  209. return nil, fmt.Errorf("target URL is required")
  210. }
  211. targetURL, err := url.Parse(r.TargetURL)
  212. if err != nil || targetURL.Scheme == "" || targetURL.Host == "" {
  213. return nil, fmt.Errorf("target URL must be a valid absolute HTTP or HTTPS URL (e.g. http://10.0.0.5:8080 or http://example.com/app1)")
  214. }
  215. // Normalize Path prefix
  216. cleanPath := path.Clean("/" + strings.TrimPrefix(strings.TrimSpace(r.Path), "/"))
  217. if r.Path == "" {
  218. cleanPath = "/"
  219. }
  220. r.Path = cleanPath
  221. // Normalize Protocol
  222. r.Protocol = strings.ToLower(strings.TrimSpace(r.Protocol))
  223. if r.Protocol == "" {
  224. r.Protocol = ProtocolBoth
  225. }
  226. m.mu.Lock()
  227. defer m.mu.Unlock()
  228. now := time.Now()
  229. var savedRule *Rule
  230. if r.ID == "" {
  231. r.ID = fmt.Sprintf("proxy-%d", now.UnixNano())
  232. r.CreatedAt = now
  233. r.UpdatedAt = now
  234. m.config.Rules = append(m.config.Rules, r)
  235. savedRule = r
  236. } else {
  237. found := false
  238. for i, existing := range m.config.Rules {
  239. if existing.ID == r.ID {
  240. r.CreatedAt = existing.CreatedAt
  241. r.UpdatedAt = now
  242. r.Stats = existing.Stats
  243. m.config.Rules[i] = r
  244. savedRule = r
  245. found = true
  246. break
  247. }
  248. }
  249. if !found {
  250. r.CreatedAt = now
  251. r.UpdatedAt = now
  252. m.config.Rules = append(m.config.Rules, r)
  253. savedRule = r
  254. }
  255. }
  256. if err := m.saveLocked(); err != nil {
  257. return nil, err
  258. }
  259. m.rebuildHandlersLocked()
  260. rc := *savedRule
  261. return &rc, nil
  262. }
  263. // DeleteRule removes a rule by ID.
  264. func (m *Manager) DeleteRule(id string) error {
  265. m.mu.Lock()
  266. defer m.mu.Unlock()
  267. found := false
  268. var updated []*Rule
  269. for _, r := range m.config.Rules {
  270. if r.ID == id {
  271. found = true
  272. continue
  273. }
  274. updated = append(updated, r)
  275. }
  276. if !found {
  277. return fmt.Errorf("rule %q not found", id)
  278. }
  279. m.config.Rules = updated
  280. if err := m.saveLocked(); err != nil {
  281. return err
  282. }
  283. m.rebuildHandlersLocked()
  284. return nil
  285. }
  286. // SetRuleState toggles enabled status for a rule.
  287. func (m *Manager) SetRuleState(id string, enabled bool) error {
  288. m.mu.Lock()
  289. defer m.mu.Unlock()
  290. found := false
  291. for _, r := range m.config.Rules {
  292. if r.ID == id {
  293. r.Enabled = enabled
  294. r.UpdatedAt = time.Now()
  295. found = true
  296. break
  297. }
  298. }
  299. if !found {
  300. return fmt.Errorf("rule %q not found", id)
  301. }
  302. if err := m.saveLocked(); err != nil {
  303. return err
  304. }
  305. m.rebuildHandlersLocked()
  306. return nil
  307. }
  308. // FindMatchingRule matches an incoming request against all enabled rules and returns the best match.
  309. func (m *Manager) FindMatchingRule(r *http.Request) (*Rule, http.Handler) {
  310. m.mu.RLock()
  311. defer m.mu.RUnlock()
  312. if !m.config.Enabled || len(m.config.Rules) == 0 {
  313. return nil, nil
  314. }
  315. host := r.Host
  316. if h, _, err := net.SplitHostPort(host); err == nil {
  317. host = h
  318. }
  319. host = strings.ToLower(strings.TrimSpace(host))
  320. isTLS := (r.TLS != nil)
  321. reqPath := r.URL.Path
  322. var bestRule *Rule
  323. var bestScore int
  324. for _, rule := range m.config.Rules {
  325. if !rule.Enabled {
  326. continue
  327. }
  328. // Protocol matching
  329. switch rule.Protocol {
  330. case ProtocolHTTP:
  331. if isTLS {
  332. continue
  333. }
  334. case ProtocolHTTPS:
  335. if !isTLS && !rule.HTTPToHTTPS {
  336. continue
  337. }
  338. }
  339. // Domain matching & scoring
  340. domainScore := 0
  341. if len(rule.Domains) == 0 {
  342. domainScore = 100 // default wildcard match
  343. } else {
  344. for _, d := range rule.Domains {
  345. dClean := strings.ToLower(strings.TrimSpace(d))
  346. if dClean == "*" {
  347. if domainScore < 100 {
  348. domainScore = 100
  349. }
  350. } else if strings.HasPrefix(dClean, "*.") {
  351. if MatchDomain(dClean, host) && domainScore < 500 {
  352. domainScore = 500
  353. }
  354. } else if MatchDomain(dClean, host) {
  355. if domainScore < 1000 {
  356. domainScore = 1000
  357. }
  358. }
  359. }
  360. }
  361. if domainScore == 0 {
  362. continue
  363. }
  364. // Path matching & scoring
  365. if !MatchPath(rule.Path, reqPath) {
  366. continue
  367. }
  368. cleanRulePath := path.Clean("/" + strings.TrimPrefix(strings.TrimSpace(rule.Path), "/"))
  369. pathScore := len(cleanRulePath) * 10
  370. if cleanRulePath == "/" {
  371. pathScore = 1
  372. }
  373. totalScore := domainScore + pathScore
  374. if totalScore > bestScore {
  375. bestScore = totalScore
  376. bestRule = rule
  377. }
  378. }
  379. if bestRule == nil {
  380. return nil, nil
  381. }
  382. handler := m.handlers[bestRule.ID]
  383. return bestRule, handler
  384. }
  385. // HandleHTTP handles an incoming HTTP/HTTPS request if a matching proxy rule is found.
  386. // Returns true if the request was handled/proxied, or false if it should fall through to DebTik Web UI / API.
  387. func (m *Manager) HandleHTTP(w http.ResponseWriter, r *http.Request) bool {
  388. rule, handler := m.FindMatchingRule(r)
  389. if rule == nil {
  390. return false
  391. }
  392. // Never intercept Let's Encrypt ACME challenges
  393. if strings.HasPrefix(r.URL.Path, "/.well-known/acme-challenge/") {
  394. return false
  395. }
  396. // Check HTTP-to-HTTPS redirect
  397. if rule.HTTPToHTTPS && r.TLS == nil {
  398. destHost := r.Host
  399. if h, _, err := net.SplitHostPort(destHost); err == nil {
  400. destHost = h
  401. }
  402. m.mu.RLock()
  403. httpsPort := m.config.HTTPSPort
  404. m.mu.RUnlock()
  405. portSuffix := ""
  406. if httpsPort != 443 && httpsPort > 0 {
  407. portSuffix = fmt.Sprintf(":%d", httpsPort)
  408. }
  409. target := fmt.Sprintf("https://%s%s%s", destHost, portSuffix, r.URL.RequestURI())
  410. http.Redirect(w, r, target, http.StatusMovedPermanently)
  411. return true
  412. }
  413. if handler == nil {
  414. return false
  415. }
  416. handler.ServeHTTP(w, r)
  417. return true
  418. }
  419. // GetTLSCertificate provides dynamic SNI certificate resolution for TLS listeners.
  420. func (m *Manager) GetTLSCertificate(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) {
  421. if m.certMgr == nil {
  422. return nil, fmt.Errorf("certificate manager not configured")
  423. }
  424. sni := strings.ToLower(clientHello.ServerName)
  425. m.mu.RLock()
  426. defer m.mu.RUnlock()
  427. // 1. Try to find an enabled rule matching this SNI host that specifies a certificate
  428. for _, rule := range m.config.Rules {
  429. if !rule.Enabled || rule.Certificate == "" {
  430. continue
  431. }
  432. for _, d := range rule.Domains {
  433. dClean := strings.ToLower(strings.TrimSpace(d))
  434. if MatchDomain(dClean, sni) {
  435. cert, err := m.certMgr.GetTLSCertificate(rule.Certificate)
  436. if err == nil {
  437. return &cert, nil
  438. }
  439. }
  440. }
  441. }
  442. // 2. Try default certificate if configured
  443. if m.config.DefaultCert != "" {
  444. cert, err := m.certMgr.GetTLSCertificate(m.config.DefaultCert)
  445. if err == nil {
  446. return &cert, nil
  447. }
  448. }
  449. // 3. Fallback to any certificate in certMgr matching SNI
  450. certsList := m.certMgr.ListCertificates()
  451. for _, c := range certsList {
  452. if c.IsCA {
  453. continue
  454. }
  455. if strings.EqualFold(c.CommonName, sni) {
  456. if cert, err := m.certMgr.GetTLSCertificate(c.Name); err == nil {
  457. return &cert, nil
  458. }
  459. }
  460. for _, san := range c.SANs {
  461. if MatchDomain(strings.ToLower(san), sni) {
  462. if cert, err := m.certMgr.GetTLSCertificate(c.Name); err == nil {
  463. return &cert, nil
  464. }
  465. }
  466. }
  467. }
  468. // 4. Fallback to the first non-CA certificate
  469. for _, c := range certsList {
  470. if !c.IsCA {
  471. if cert, err := m.certMgr.GetTLSCertificate(c.Name); err == nil {
  472. return &cert, nil
  473. }
  474. }
  475. }
  476. return nil, fmt.Errorf("no TLS certificate found for server name %q", sni)
  477. }
  478. // Stop gracefully shuts down any standalone proxy listeners.
  479. func (m *Manager) Stop() {
  480. m.httpsMu.Lock()
  481. if m.httpsServer != nil {
  482. ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
  483. _ = m.httpsServer.Shutdown(ctx)
  484. cancel()
  485. m.httpsServer = nil
  486. m.httpsRunning = false
  487. }
  488. m.httpsMu.Unlock()
  489. m.httpMu.Lock()
  490. if m.httpServer != nil {
  491. ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
  492. _ = m.httpServer.Shutdown(ctx)
  493. cancel()
  494. m.httpServer = nil
  495. m.httpRunning = false
  496. }
  497. m.httpMu.Unlock()
  498. }