@@ -15,14 +15,20 @@ import (
1515 "github.com/sokol/system-control/internal/pkg/validate"
1616)
1717
18+ // SettingsProvider supplies global settings the nginx config depends on.
19+ type SettingsProvider interface {
20+ ForceTLS12 () bool
21+ }
22+
1823type Service struct {
1924 repo * Repository
2025 sitesDir string
2126 backendPort int
27+ settings SettingsProvider
2228}
2329
24- func NewService (repo * Repository , sitesDir string , backendPort int ) * Service {
25- return & Service {repo : repo , sitesDir : sitesDir , backendPort : backendPort }
30+ func NewService (repo * Repository , sitesDir string , backendPort int , settings SettingsProvider ) * Service {
31+ return & Service {repo : repo , sitesDir : sitesDir , backendPort : backendPort , settings : settings }
2632}
2733
2834func (s * Service ) GetAll () ([]Domain , error ) {
@@ -384,18 +390,23 @@ func (s *Service) configPath(domain string) string {
384390 return filepath .Join (s .sitesDir , fmt .Sprintf ("sc_%s.conf" , domain ))
385391}
386392
387- func (s * Service ) writeAndReload (domain * Domain ) error {
393+ // writeConfig renders and writes the nginx config for a domain and validates
394+ // it with `nginx -t`, rolling back on test failure (restoring the previous
395+ // config if one existed, otherwise removing the brand-new file). It does NOT
396+ // reload nginx — callers reload once they have written all configs.
397+ func (s * Service ) writeConfig (domain * Domain ) error {
388398 if runtime .GOOS != "linux" {
389399 slog .Warn ("nginx config write skipped on non-Linux" , "domain" , domain .Domain )
390400 return nil
391401 }
392402
393- content , err := renderConfig (domain , s .backendPort )
403+ content , err := renderConfig (domain , s .backendPort , s . settings . ForceTLS12 () )
394404 if err != nil {
395405 return fmt .Errorf ("render config: %w" , err )
396406 }
397407
398408 path := s .configPath (domain .Domain )
409+ prev , prevErr := os .ReadFile (path ) // prevErr != nil => no prior config existed
399410 if err := os .WriteFile (path , content , 0644 ); err != nil {
400411 return fmt .Errorf ("write config file: %w" , err )
401412 }
@@ -404,15 +415,74 @@ func (s *Service) writeAndReload(domain *Domain) error {
404415 cmd := exec .Command ("nginx" , "-t" )
405416 output , err := cmd .CombinedOutput ()
406417 if err != nil {
407- // Rollback: remove bad config
408- os .Remove (path )
418+ // Roll back: restore the previous working config if one existed,
419+ // otherwise remove the brand-new (bad) file. Prevents a failed
420+ // rebuild from taking an already-online domain offline.
421+ if prevErr == nil {
422+ if wErr := os .WriteFile (path , prev , 0644 ); wErr != nil {
423+ slog .Error ("failed to restore previous nginx config after failed test" , "error" , wErr , "domain" , domain .Domain )
424+ }
425+ } else {
426+ os .Remove (path )
427+ }
409428 return fmt .Errorf ("nginx config test failed: %s: %w" , string (output ), err )
410429 }
411430
431+ return nil
432+ }
433+
434+ func (s * Service ) writeAndReload (domain * Domain ) error {
435+ if err := s .writeConfig (domain ); err != nil {
436+ return err
437+ }
412438 s .reloadNginx ()
413439 return nil
414440}
415441
442+ // rebuildTargets returns the domains whose nginx config depends on global
443+ // SSL settings: enabled domains that have SSL. Non-SSL domains are skipped
444+ // because ForceTLS12 / http2 only affect the SSL server block.
445+ func rebuildTargets (domains []Domain ) []* Domain {
446+ var targets []* Domain
447+ for i := range domains {
448+ d := & domains [i ]
449+ if d .Enabled && d .SSLEnabled {
450+ targets = append (targets , d )
451+ }
452+ }
453+ return targets
454+ }
455+
456+ // RebuildAll re-renders and reloads nginx configs for all SSL-enabled domains.
457+ // Used when a global setting (e.g. force TLS 1.2) changes. It reloads nginx once
458+ // after writing all configs rather than per domain.
459+ func (s * Service ) RebuildAll () error {
460+ if runtime .GOOS != "linux" {
461+ return nil
462+ }
463+ domains , err := s .repo .GetAll ()
464+ if err != nil {
465+ return fmt .Errorf ("get domains: %w" , err )
466+ }
467+ written := 0
468+ for _ , d := range rebuildTargets (domains ) {
469+ if err := s .writeConfig (d ); err != nil {
470+ slog .Error ("failed to rebuild nginx config" , "error" , err , "domain" , d .Domain )
471+ continue
472+ }
473+ written ++
474+ }
475+ if written > 0 {
476+ s .reloadNginx ()
477+ }
478+ return nil
479+ }
480+
481+ // OnSettingsChanged implements settings.ChangeListener: rebuild all configs.
482+ func (s * Service ) OnSettingsChanged () error {
483+ return s .RebuildAll ()
484+ }
485+
416486func (s * Service ) removeConfig (domain string ) {
417487 os .Remove (s .configPath (domain ))
418488}
0 commit comments