diff --git a/internal/home/authhttp.go b/internal/home/authhttp.go index 39610740..1f3a325a 100644 --- a/internal/home/authhttp.go +++ b/internal/home/authhttp.go @@ -374,32 +374,11 @@ func (mw *authMiddlewareDefault) Wrap(h http.Handler) (wrapped http.Handler) { } path := r.URL.Path - u, err := mw.userFromRequest(ctx, r) - if err != nil { - mw.logger.ErrorContext(ctx, "retrieving user from request", slogutil.KeyError, err) - } - - if u != nil { - if path == "/login.html" { - http.Redirect(w, r, "/", http.StatusFound) - - return - } - - h.ServeHTTP(w, r.WithContext(withWebUser(ctx, u))) - + if mw.handleAuthenticatedUser(ctx, w, r, h, path) { return } - if isPublicResource(path) { - h.ServeHTTP(w, r) - - return - } - - if path == "/" || path == "/index.html" { - http.Redirect(w, r, "login.html", http.StatusFound) - + if mw.handlePublicAccess(w, r, h, path) { return } @@ -407,6 +386,58 @@ func (mw *authMiddlewareDefault) Wrap(h http.Handler) (wrapped http.Handler) { }) } +// handleAuthenticatedUser tries to get user from request and processes request +// if user was successfully authenticated. Returns true if request was handled. +func (mw *authMiddlewareDefault) handleAuthenticatedUser( + ctx context.Context, + w http.ResponseWriter, + r *http.Request, + h http.Handler, + path string, +) (ok bool) { + u, err := mw.userFromRequest(ctx, r) + if err != nil { + mw.logger.ErrorContext(ctx, "retrieving user from request", slogutil.KeyError, err) + } + + if u == nil { + return false + } + + if path == "/login.html" { + http.Redirect(w, r, "/", http.StatusFound) + + return true + } + + h.ServeHTTP(w, r.WithContext(withWebUser(ctx, u))) + + return true +} + +// handlePublicAccess handles request if user is trying to access public or root +// pages. +func (mw *authMiddlewareDefault) handlePublicAccess( + w http.ResponseWriter, + r *http.Request, + h http.Handler, + path string, +) (ok bool) { + if isPublicResource(path) { + h.ServeHTTP(w, r) + + return true + } + + if path == "/" || path == "/index.html" { + http.Redirect(w, r, "login.html", http.StatusFound) + + return true + } + + return false +} + // needsAuthentication returns true if there are stored web users and requests // should be authenticated first. func (mw *authMiddlewareDefault) needsAuthentication(ctx context.Context) (ok bool) { diff --git a/internal/home/controlupdate.go b/internal/home/controlupdate.go index 0bfb0fff..06acff43 100644 --- a/internal/home/controlupdate.go +++ b/internal/home/controlupdate.go @@ -228,56 +228,60 @@ func finishUpdate( cleanup(ctx) cleanupAlways() - var err error if runtime.GOOS == "windows" { - if runningAsService { - // NOTE: We can't restart the service via "kardianos/service" - // package, because it kills the process first we can't start a new - // instance, because Windows doesn't allow it. - // - // TODO(a.garipov): Recheck the claim above. - var cmd executil.Command - cmd, err = cmdCons.New(ctx, &executil.CommandConfig{ - Path: "cmd", - Args: []string{"/c", "net stop AdGuardHome & net start AdGuardHome"}, - }) - if err != nil { - panic(fmt.Errorf("constructing cmd: %w", err)) - } - - err = cmd.Start(ctx) - if err != nil { - panic(fmt.Errorf("restarting service: %w", err)) - } - - os.Exit(osutil.ExitCodeSuccess) - } - - l.InfoContext(ctx, "restarting", "exec_path", execPath, "args", os.Args[1:]) - - var cmd executil.Command - cmd, err = cmdCons.New(ctx, &executil.CommandConfig{ - Path: execPath, - Args: os.Args[1:], - Stdin: os.Stdin, - Stdout: os.Stdout, - Stderr: os.Stderr, - }) - if err != nil { - panic(fmt.Errorf("constructing cmd: %w", err)) - } - - err = cmd.Start(ctx) - if err != nil { - panic(fmt.Errorf("restarting: %w", err)) - } + finalizeWindowsUpdate(ctx, l, cmdCons, execPath, runningAsService) os.Exit(osutil.ExitCodeSuccess) } + var err error l.InfoContext(ctx, "restarting", "exec_path", execPath, "args", os.Args[1:]) err = syscall.Exec(execPath, os.Args, os.Environ()) if err != nil { panic(fmt.Errorf("restarting: %w", err)) } } + +// finalizeWindowsUpdate completes an update procedure on windows. l and +// cmdCons must not be nil. +func finalizeWindowsUpdate(ctx context.Context, + l *slog.Logger, + cmdCons executil.CommandConstructor, + execPath string, + runningAsService bool, +) { + var commandConf *executil.CommandConfig + + if runningAsService { + // NOTE: We can't restart the service via "kardianos/service" package, + // because it kills the process first we can't start a new instance, + // because Windows doesn't allow it. + // + // TODO(a.garipov): Recheck the claim above. + commandConf = &executil.CommandConfig{ + Path: "cmd", + Args: []string{"/c", "net stop AdGuardHome & net start AdGuardHome"}, + } + } else { + commandConf = &executil.CommandConfig{ + Path: execPath, + Args: os.Args[1:], + Stdin: os.Stdin, + Stdout: os.Stdout, + Stderr: os.Stderr, + } + } + + l.InfoContext(ctx, "restarting", "exec_path", execPath, "args", os.Args[1:]) + + var cmd executil.Command + cmd, err := cmdCons.New(ctx, commandConf) + if err != nil { + panic(fmt.Errorf("constructing cmd: %w", err)) + } + + err = cmd.Start(ctx) + if err != nil { + panic(fmt.Errorf("restarting: %w", err)) + } +} diff --git a/internal/home/home.go b/internal/home/home.go index 9690f7ba..101919b2 100644 --- a/internal/home/home.go +++ b/internal/home/home.go @@ -695,100 +695,29 @@ func run( workDir string, confPath string, ) { - ls := getLogSettings(ctx, slogLogger, opts, workDir, confPath) - - // Configure log level and output. - err := configureLogger(ls, workDir) - fatalOnError(err) - - // Print the first message after logger is configured. - slogLogger.InfoContext(ctx, "starting adguard home", "version", version.Full()) - slogLogger.DebugContext(ctx, "current working directory", "path", workDir) - if opts.runningAsService { - slogLogger.InfoContext(ctx, "adguard home is running as a service") - } - - aghtls.Init(ctx, slogLogger.With(slogutil.KeyPrefix, "aghtls")) + initEnvironment(ctx, opts, slogLogger, workDir, confPath) isFirstRun := detectFirstRun(ctx, slogLogger, workDir, confPath) - err = setupContext(ctx, slogLogger, opts, workDir, confPath, isFirstRun) - fatalOnError(err) - - err = configureOS(config) - fatalOnError(err) - - // Clients package uses filtering package's static data - // (filtering.BlockedSvcKnown()), so we have to initialize filtering static - // data first, but also to avoid relying on automatic Go init() function. - filtering.InitModule(ctx, slogLogger) - - confModifier := newDefaultConfigModifier( - config, - slogLogger.With(slogutil.KeyPrefix, "config_modifier"), - workDir, - confPath, - ) - mw := &webMw{} mux := http.NewServeMux() httpReg := aghhttp.NewDefaultRegistrar(mux, mw.wrap) - err = initContextClients(ctx, slogLogger, sigHdlr, confModifier, httpReg, workDir) - fatalOnError(err) - - tlsMgrLogger := slogLogger.With(slogutil.KeyPrefix, "tls_manager") - - tlsMgr, err := newTLSManager(ctx, &tlsManagerConfig{ - logger: tlsMgrLogger, - confModifier: confModifier, - httpReg: httpReg, - tlsSettings: config.TLS, - servePlainDNS: config.DNS.ServePlainDNS, - }) - if err != nil { - tlsMgrLogger.ErrorContext(ctx, "initializing", slogutil.KeyError, err) - confModifier.Apply(ctx) - } - - confModifier.setTLSManager(tlsMgr) - - err = setupDNSFilteringConf( + confModifier, tlsMgr := initFiltering( ctx, slogLogger, - config.Filtering, - tlsMgr, - confModifier, - httpReg, + opts, + isFirstRun, + sigHdlr, workDir, + confPath, + httpReg, ) - fatalOnError(err) - err = setupOpts(opts) - fatalOnError(err) - - execPath, err := os.Executable() - fatalOnError(errors.Annotate(err, "getting executable path: %w")) - - updLogger := slogLogger.With(slogutil.KeyPrefix, "updater") - upd, isCustomURL := newUpdater(ctx, updLogger, config, workDir, confPath, execPath) - - // TODO(e.burkov): This could be made earlier, probably as the option's - // effect. - cmdlineUpdate(ctx, updLogger, opts, upd, tlsMgr, isFirstRun) - - if !isFirstRun { - // Save the updated config. - err = config.write(ctx, slogLogger, nil, nil, workDir, confPath) - fatalOnError(err) - - if config.HTTPConfig.Pprof.Enabled { - startPprof(slogLogger, config.HTTPConfig.Pprof.Port) - } - } + upd, isCustomURL := initUpdate(ctx, slogLogger, opts, tlsMgr, isFirstRun, workDir, confPath) dataDirPath := filepath.Join(workDir, dataDir) - err = os.MkdirAll(dataDirPath, aghos.DefaultPermDir) + err := os.MkdirAll(dataDirPath, aghos.DefaultPermDir) fatalOnError(errors.Annotate(err, "creating DNS data dir at %s: %w", dataDirPath)) auth, err := initUsers(ctx, slogLogger, workDir, opts.glinetMode) @@ -825,25 +754,7 @@ func run( fatalOnError(err) if !isFirstRun { - err = initDNS(ctx, slogLogger, tlsMgr, confModifier, httpReg, statsDir, querylogDir) - fatalOnError(err) - - tlsMgr.start(ctx) - - go func() { - startErr := startDNSServer() - if startErr != nil { - closeDNSServer(ctx) - fatalOnError(startErr) - } - }() - - if globalContext.dhcpServer != nil { - err = globalContext.dhcpServer.Start(ctx) - if err != nil { - slogLogger.ErrorContext(ctx, "starting dhcp server", slogutil.KeyError, err) - } - } + runDNSServer(ctx, slogLogger, tlsMgr, confModifier, statsDir, querylogDir, httpReg) } if !opts.noPermCheck { @@ -856,6 +767,171 @@ func run( <-done } +// runDNSServer initializes and starts DNS and DHCP servers if this is not the +// first run. httpReg, slogLogger, tlsMgr and confModifier must not be nil. +func runDNSServer( + ctx context.Context, + slogLogger *slog.Logger, + tlsMgr *tlsManager, + confModifier *defaultConfigModifier, + statsDir string, + querylogDir string, + httpReg *aghhttp.DefaultRegistrar, +) { + err := initDNS(ctx, slogLogger, tlsMgr, confModifier, httpReg, statsDir, querylogDir) + fatalOnError(err) + + tlsMgr.start(ctx) + + go func() { + startErr := startDNSServer() + if startErr != nil { + closeDNSServer(ctx) + fatalOnError(startErr) + } + }() + + if globalContext.dhcpServer != nil { + err = globalContext.dhcpServer.Start(ctx) + if err != nil { + slogLogger.ErrorContext(ctx, "starting dhcp server", slogutil.KeyError, err) + } + } +} + +// initFiltering configures the core filtering and TLS subsystems. Returns a +// configuration modifier and the initialized TLS manager. slogLogger, sigHdlr +// and httpReg must not be nil. +func initFiltering( + ctx context.Context, + slogLogger *slog.Logger, + opts options, + isFirstRun bool, + sigHdlr *signalHandler, + workDir string, + confPath string, + httpReg *aghhttp.DefaultRegistrar, +) (confModifier *defaultConfigModifier, tlsMgr *tlsManager) { + err := setupContext(ctx, slogLogger, opts, workDir, confPath, isFirstRun) + fatalOnError(err) + + err = configureOS(config) + fatalOnError(err) + + // Clients package uses filtering package's static data + // (filtering.BlockedSvcKnown()), so we have to initialize filtering static + // data first, but also to avoid relying on automatic Go init() function. + filtering.InitModule(ctx, slogLogger) + + confModifier = newDefaultConfigModifier( + config, + slogLogger.With(slogutil.KeyPrefix, "config_modifier"), + workDir, + confPath, + ) + + err = initContextClients(ctx, slogLogger, sigHdlr, confModifier, httpReg, workDir) + fatalOnError(err) + + tlsMgrLogger := slogLogger.With(slogutil.KeyPrefix, "tls_manager") + + tlsMgr, err = newTLSManager(ctx, &tlsManagerConfig{ + logger: tlsMgrLogger, + confModifier: confModifier, + httpReg: httpReg, + tlsSettings: config.TLS, + servePlainDNS: config.DNS.ServePlainDNS, + }) + if err != nil { + tlsMgrLogger.ErrorContext(ctx, "initializing", slogutil.KeyError, err) + confModifier.Apply(ctx) + } + + confModifier.setTLSManager(tlsMgr) + + err = setupDNSFilteringConf( + ctx, + slogLogger, + config.Filtering, + tlsMgr, + confModifier, + httpReg, + workDir, + ) + fatalOnError(err) + + err = setupOpts(opts) + fatalOnError(err) + + return confModifier, tlsMgr +} + +// initUpdate configures and runs update of this application. slogLogger and +// tlsMgr must not be nil. +func initUpdate( + ctx context.Context, + slogLogger *slog.Logger, + opts options, + tlsMgr *tlsManager, + isFirstRun bool, + workDir string, + confPath string, +) (upd *updater.Updater, isCustomURL bool) { + execPath, err := os.Executable() + fatalOnError(errors.Annotate(err, "getting executable path: %w")) + + updLogger := slogLogger.With(slogutil.KeyPrefix, "updater") + upd, isCustomURL = newUpdater( + ctx, + updLogger, + config, + workDir, + confPath, + execPath, + ) + + // TODO(e.burkov): This could be made earlier, probably as the option's + // effect. + cmdlineUpdate(ctx, updLogger, opts, upd, tlsMgr, isFirstRun) + + if !isFirstRun { + // Save the updated config. + err = config.write(ctx, slogLogger, nil, nil, workDir, confPath) + fatalOnError(err) + + if config.HTTPConfig.Pprof.Enabled { + startPprof(slogLogger, config.HTTPConfig.Pprof.Port) + } + } + + return upd, isCustomURL +} + +// initEnvironment inits working environment. opts and slogLogger must not be +// nil. +func initEnvironment( + ctx context.Context, + opts options, + slogLogger *slog.Logger, + workDir, + confPath string, +) { + ls := getLogSettings(ctx, slogLogger, opts, workDir, confPath) + + // Configure log level and output. + err := configureLogger(ls, workDir) + fatalOnError(err) + + // Print the first message after logger is configured. + slogLogger.InfoContext(ctx, "starting adguard home", "version", version.Full()) + slogLogger.DebugContext(ctx, "current working directory", "path", workDir) + if opts.runningAsService { + slogLogger.InfoContext(ctx, "adguard home is running as a service") + } + + aghtls.Init(ctx, slogLogger.With(slogutil.KeyPrefix, "aghtls")) +} + // newUpdater creates a new AdGuard Home updater. l and conf must not be nil. // workDir, confPath, and execPath must not be empty. isCustomURL is true if // the user has specified a custom version announcement URL. diff --git a/internal/home/options.go b/internal/home/options.go index e0750e3e..41d84860 100644 --- a/internal/home/options.go +++ b/internal/home/options.go @@ -2,8 +2,10 @@ package home import ( "fmt" + "iter" "net/netip" "os" + "slices" "strconv" "strings" @@ -384,41 +386,89 @@ func printHelp(exec string) { // parseCmdOpts parses the command-line arguments into options and effects. func parseCmdOpts(cmdName string, args []string) (o options, eff effect, err error) { - // Don't use range since the loop changes the loop variable. - argsLen := len(args) - for i := 0; i < len(args); i++ { - arg := args[i] - isKnown := false - for _, opt := range cmdLineOpts { - isKnown = argMatches(opt, arg) - if !isKnown { - continue - } + next, stop := iter.Pull2(slices.All(args)) + defer stop() - if opt.updateWithValue != nil { - i++ - if i >= argsLen { - return o, eff, fmt.Errorf("got %s without argument", arg) - } - - o, err = opt.updateWithValue(o, args[i]) - } else { - o, eff, err = updateOptsNoValue(o, eff, opt, cmdName) - } - - if err != nil { - return o, eff, fmt.Errorf("applying option %s: %w", arg, err) - } - - break - } - - if !isKnown { - return o, eff, fmt.Errorf("unknown option %s", arg) + for i, arg, ok := next(); ok; i, arg, ok = next() { + o, eff, err = parseArg(cmdName, next, o, eff, arg) + if err != nil { + return o, eff, fmt.Errorf("parsing arg at index %d: %w", i, err) } } - return o, eff, err + return o, eff, nil +} + +// parseArg parses command-line argument into options and effects. next and +// eff must not be nil. +func parseArg( + cmdName string, + next func() (int, string, bool), + o options, + eff effect, + arg string, +) (newOpt options, newEff effect, err error) { + opt, found := findMatchingOpt(arg) + if !found { + return o, eff, fmt.Errorf("unknown option %s", arg) + } + + if opt.updateWithValue != nil { + return applyOptWithValue(opt, next, o, eff, arg) + } + + return applyOptNoValue(opt, cmdName, o, eff, arg) +} + +// applyOptNoValue applies option with no value. eff must not be +// nil. +func applyOptNoValue( + opt cmdLineOpt, + cmdName string, + o options, + eff effect, + arg string, +) (newOpt options, newEff effect, err error) { + newOpts, newEff, err := updateOptsNoValue(o, eff, opt, cmdName) + if err != nil { + return o, eff, fmt.Errorf("applying option %s: %w", arg, err) + } + + return newOpts, newEff, nil +} + +// applyOptWithValue applies argument with value. next and eff must not +// be nil. +func applyOptWithValue( + opt cmdLineOpt, + next func() (int, string, bool), + o options, + eff effect, + arg string, +) (newOpt options, newEff effect, err error) { + _, val, ok := next() + if !ok { + return o, eff, fmt.Errorf("got %s without argument", arg) + } + + newOpts, err := opt.updateWithValue(o, val) + if err != nil { + return o, eff, fmt.Errorf("applying option %s: %w", arg, err) + } + + return newOpts, eff, nil +} + +// findMatchingOpt returns cmdLineOpt which matches the given arg. ok indicates +// whether the appropriate option was found. +func findMatchingOpt(arg string) (opt cmdLineOpt, ok bool) { + for _, opt := range cmdLineOpts { + if argMatches(opt, arg) { + return opt, true + } + } + + return cmdLineOpt{}, false } // argMatches returns true if arg matches command-line option opt. diff --git a/internal/home/service.go b/internal/home/service.go index 9f4749ad..1bfa30db 100644 --- a/internal/home/service.go +++ b/internal/home/service.go @@ -390,16 +390,11 @@ func handleServiceInstallCommand( } } -// handleServiceUninstallCommand handles service "uninstall" command. +// handleServiceUninstallCommand handles service "uninstall" command. l and s +// must not be nil. func handleServiceUninstallCommand(ctx context.Context, l *slog.Logger, s service.Service) { if aghos.IsOpenWrt() { - // On OpenWrt it is important to run disable command first - // as it will remove the symlink - _, err := runInitdCommand(ctx, "disable") - if err != nil { - l.ErrorContext(ctx, "running init disable", slogutil.KeyError, err) - os.Exit(osutil.ExitCodeFailure) - } + handleOpenWrtUninstall(ctx, l) } if err := svcAction(ctx, l, s, "stop"); err != nil { @@ -408,20 +403,39 @@ func handleServiceUninstallCommand(ctx context.Context, l *slog.Logger, s servic if err := svcAction(ctx, l, s, "uninstall"); err != nil { l.ErrorContext(ctx, "executing action uninstall", slogutil.KeyError, err) + os.Exit(osutil.ExitCodeFailure) } if runtime.GOOS == "darwin" { - // Remove log files on cleanup and log errors. - err := os.Remove(launchdStdoutPath) - if err != nil && !errors.Is(err, os.ErrNotExist) { - l.WarnContext(ctx, "removing stdout file", slogutil.KeyError, err) - } + handleDarwinUninstall(ctx, l) + } +} - err = os.Remove(launchdStderrPath) - if err != nil && !errors.Is(err, os.ErrNotExist) { - l.WarnContext(ctx, "removing stderr file", slogutil.KeyError, err) - } +// handleOpenWrtUninstall handles service "uninstall" command for OpenWrt. +// Exits on error. l must not be nil. +func handleOpenWrtUninstall(ctx context.Context, l *slog.Logger) { + // On OpenWrt it is important to run disable command first as it will remove + // the symlink. + _, err := runInitdCommand(ctx, "disable") + if err != nil { + l.ErrorContext(ctx, "running init disable", slogutil.KeyError, err) + os.Exit(osutil.ExitCodeFailure) + } +} + +// handleDarwinUninstall handles service "uninstall" command for Darwin. l +// must not be nil. +func handleDarwinUninstall(ctx context.Context, l *slog.Logger) { + // Remove log files on cleanup and log errors. + err := os.Remove(launchdStdoutPath) + if err != nil && !errors.Is(err, os.ErrNotExist) { + l.WarnContext(ctx, "removing stdout file", slogutil.KeyError, err) + } + + err = os.Remove(launchdStderrPath) + if err != nil && !errors.Is(err, os.ErrNotExist) { + l.WarnContext(ctx, "removing stderr file", slogutil.KeyError, err) } } diff --git a/internal/home/web.go b/internal/home/web.go index 21a3f3f0..70e816bf 100644 --- a/internal/home/web.go +++ b/internal/home/web.go @@ -318,72 +318,92 @@ func (web *webAPI) close(ctx context.Context) { web.logger.InfoContext(ctx, "stopped http server") } +// tlsServerLoop implements retry logic for http server start. func (web *webAPI) tlsServerLoop(ctx context.Context) { defer slogutil.RecoverAndExit(ctx, web.logger, osutil.ExitCodeFailure) for { - web.httpsServer.cond.L.Lock() - if web.httpsServer.inShutdown { - web.httpsServer.cond.L.Unlock() - break - } - - // this mechanism doesn't let us through until all conditions are met - for !web.httpsServer.enabled { // sleep until necessary data is supplied - web.httpsServer.cond.Wait() - if web.httpsServer.inShutdown { - web.httpsServer.cond.L.Unlock() - return - } - } - - web.httpsServer.cond.L.Unlock() - - var portHTTPS uint16 - func() { - config.RLock() - defer config.RUnlock() - - portHTTPS = config.TLS.PortHTTPS - }() - - addr := netip.AddrPortFrom(web.conf.BindAddr.Addr(), portHTTPS).String() - logger := web.baseLogger.With(loggerKeyServer, "https") - - // TODO(a.garipov): Remove other logs like this in other code. - logMw := httputil.NewLogMiddleware(logger, slog.LevelDebug) - hdlr := logMw.Wrap(withMiddlewares(web.conf.mux, limitRequestBody)) - - web.httpsServer.server = &http.Server{ - Addr: addr, - Handler: web.auth.middleware().Wrap(hdlr), - TLSConfig: &tls.Config{ - Certificates: []tls.Certificate{web.httpsServer.cert}, - RootCAs: web.tlsManager.rootCerts, - CipherSuites: web.tlsManager.customCipherIDs, - MinVersion: tls.VersionTLS12, - }, - ReadTimeout: web.conf.ReadTimeout, - ReadHeaderTimeout: web.conf.ReadHeaderTimeout, - WriteTimeout: web.conf.WriteTimeout, - ErrorLog: slog.NewLogLogger(logger.Handler(), slog.LevelError), - } - - printHTTPAddresses(urlutil.SchemeHTTPS, web.tlsManager) - - if web.conf.serveHTTP3 { - go web.mustStartHTTP3(ctx, addr) - } - - logger.InfoContext(ctx, "starting https server") - err := web.httpsServer.server.ListenAndServeTLS("", "") - if !errors.Is(err, http.ErrServerClosed) { - cleanupAlways() - panic(fmt.Errorf("https: %w", err)) + shouldContinue := web.serveTLS(ctx) + if !shouldContinue { + return } } } +// serveTLS initializes and starts the HTTPS server. Returns true when next +// retry is necessary. +func (web *webAPI) serveTLS(ctx context.Context) (next bool) { + if !web.waitForTLSReady() { + return false + } + + var portHTTPS uint16 + func() { + config.RLock() + defer config.RUnlock() + + portHTTPS = config.TLS.PortHTTPS + }() + + addr := netip.AddrPortFrom(web.conf.BindAddr.Addr(), portHTTPS).String() + logger := web.baseLogger.With(loggerKeyServer, "https") + + // TODO(a.garipov): Remove other logs like this in other code. + logMw := httputil.NewLogMiddleware(logger, slog.LevelDebug) + hdlr := logMw.Wrap(withMiddlewares(web.conf.mux, limitRequestBody)) + + web.httpsServer.server = &http.Server{ + Addr: addr, + Handler: web.auth.middleware().Wrap(hdlr), + TLSConfig: &tls.Config{ + Certificates: []tls.Certificate{web.httpsServer.cert}, + RootCAs: web.tlsManager.rootCerts, + CipherSuites: web.tlsManager.customCipherIDs, + MinVersion: tls.VersionTLS12, + }, + ReadTimeout: web.conf.ReadTimeout, + ReadHeaderTimeout: web.conf.ReadHeaderTimeout, + WriteTimeout: web.conf.WriteTimeout, + ErrorLog: slog.NewLogLogger(logger.Handler(), slog.LevelError), + } + + printHTTPAddresses(urlutil.SchemeHTTPS, web.tlsManager) + + if web.conf.serveHTTP3 { + go web.mustStartHTTP3(ctx, addr) + } + + logger.InfoContext(ctx, "starting https server") + err := web.httpsServer.server.ListenAndServeTLS("", "") + if !errors.Is(err, http.ErrServerClosed) { + cleanupAlways() + panic(fmt.Errorf("https: %w", err)) + } + + return true +} + +// waitForTLSReady blocks until the HTTPS server is enabled or a shutdown signal +// is received. Returns true when server is ready. +func (web *webAPI) waitForTLSReady() (ok bool) { + web.httpsServer.cond.L.Lock() + defer web.httpsServer.cond.L.Unlock() + + if web.httpsServer.inShutdown { + return false + } + + // this mechanism doesn't let us through until all conditions are met + for !web.httpsServer.enabled { // sleep until necessary data is supplied + web.httpsServer.cond.Wait() + if web.httpsServer.inShutdown { + return false + } + } + + return true +} + func (web *webAPI) mustStartHTTP3(ctx context.Context, address string) { defer slogutil.RecoverAndExit(ctx, web.logger, osutil.ExitCodeFailure) diff --git a/scripts/make/go-lint.sh b/scripts/make/go-lint.sh index 400c7449..2811f251 100644 --- a/scripts/make/go-lint.sh +++ b/scripts/make/go-lint.sh @@ -172,10 +172,6 @@ run_linter gocognit --over='20' \ ./internal/querylog/ \ ; -run_linter gocognit --over='19' \ - ./internal/home/ \ - ; - run_linter gocognit --over='14' \ ./internal/dhcpd \ ; @@ -195,6 +191,7 @@ run_linter gocognit --over='10' \ ./internal/dhcpsvc \ ./internal/dnsforward/ \ ./internal/filtering/ \ + ./internal/home/ \ ./internal/ipset \ ./internal/next/ \ ./internal/rdns/ \