lolly/internal/app/app_common.go
xfy e733273139 fix(server,app,proxy,resolver,middleware,lua): add nil guards and safe defaults
- server: reject Start() when config is nil to prevent panic
- app_common: guard empty Servers slice in initHTTP2/3 and logServerAddresses
- proxy/health: handle nil HealthCheckConfig with defaults
- resolver: handle nil ResolverConfig by returning noopResolver
- middleware/headers: skip UpdateConfig when cfg is nil
- middleware/sliding_window: enforce minimum window duration of 1s
- lua/api_log: map EMERG/ALERT/CRIT to Error() instead of Fatal()
  to prevent Lua scripts from killing the entire server process
2026-06-11 16:23:04 +08:00

271 lines
7.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Package app 提供 Lolly 应用程序的生命周期管理和命令行入口。
//
// 包含应用通用逻辑相关的工具函数。
//
// 作者xfy
package app
import (
"fmt"
"net"
"os"
"sync"
"time"
"rua.plus/lolly/internal/config"
"rua.plus/lolly/internal/http2"
"rua.plus/lolly/internal/http3"
"rua.plus/lolly/internal/logging"
"rua.plus/lolly/internal/resolver"
"rua.plus/lolly/internal/server"
"rua.plus/lolly/internal/stream"
"rua.plus/lolly/internal/variable"
)
// App manages the server lifecycle, including HTTP, HTTP/3, Stream servers and graceful upgrades.
type App struct {
resv resolver.Resolver
cfg *config.Config
srv *server.Server
http3Srv *http3.Server
http2Srv *http2.Server
streamSrv *stream.Server
upgradeMgr *server.UpgradeManager
logger *logging.AppLogger
cfgPath string
pidFile string
logFile string
listeners []net.Listener
ready chan struct{}
readyOnce sync.Once
}
// NewApp creates a new App instance with the given config path.
func NewApp(cfgPath string) *App {
return &App{
cfgPath: cfgPath,
ready: make(chan struct{}),
}
}
// WaitReady blocks until the app has completed initialization.
func (a *App) WaitReady() {
<-a.ready
}
// signalReady closes the ready channel to unblock callers of WaitReady.
func (a *App) signalReady() {
a.readyOnce.Do(func() { close(a.ready) })
}
// SetPidFile sets the path to the PID file for the app.
func (a *App) SetPidFile(path string) {
a.pidFile = path
}
// SetLogFile sets the path to the log file for the app.
func (a *App) SetLogFile(path string) {
a.logFile = path
}
// loadAndValidateConfig loads configuration and initializes the logger.
func (a *App) loadAndValidateConfig() error {
cfg, err := config.Load(a.cfgPath)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to load config: %v\n", err)
return err
}
a.cfg = cfg
a.logger = logging.NewAppLogger(&cfg.Logging)
return nil
}
// initVariables loads global variables from configuration.
func (a *App) initVariables() {
variable.SetGlobalVariables(a.cfg.Variables.Set)
if len(a.cfg.Variables.Set) > 0 {
a.logger.LogStartup("Global variables loaded", map[string]string{
"count": fmt.Sprintf("%d", len(a.cfg.Variables.Set)),
})
}
}
// logServerAddresses logs the listening addresses based on server mode.
func (a *App) logServerAddresses() {
a.logger.LogStartup("Config loaded successfully", map[string]string{"config_path": a.cfgPath})
if len(a.cfg.Servers) == 0 {
return
}
mode := a.cfg.GetMode()
if mode == config.ServerModeMultiServer {
for i, srv := range a.cfg.Servers {
a.logger.LogStartup("Listening address", map[string]string{
"index": fmt.Sprintf("[%d]", i),
"listen": srv.Listen,
"name": srv.Name,
})
}
} else {
a.logger.LogStartup("Listening address", map[string]string{"listen": a.cfg.Servers[0].Listen})
}
}
// initResolver initializes the DNS resolver if enabled.
func (a *App) initResolver() {
if a.cfg.Resolver.Enabled {
a.resv = resolver.New(&a.cfg.Resolver)
a.logger.LogStartup("DNS resolver enabled", map[string]string{
"addresses": fmt.Sprintf("%v", a.cfg.Resolver.Addresses),
"ttl": a.cfg.Resolver.TTL().String(),
})
}
}
// initServer creates the main server and sets the resolver.
func (a *App) initServer() {
a.srv = server.New(a.cfg)
if a.resv != nil {
a.srv.SetResolver(a.resv)
}
if len(a.listeners) > 0 {
a.srv.SetListeners(a.listeners)
}
}
// initStreamServers configures and starts stream servers.
func (a *App) initStreamServers() {
if len(a.cfg.Stream) == 0 {
return
}
a.streamSrv = stream.NewServer()
for _, sc := range a.cfg.Stream {
targets := make([]stream.TargetSpec, len(sc.Upstream.Targets))
for i, t := range sc.Upstream.Targets {
targets[i] = stream.TargetSpec{
Addr: t.Addr,
Weight: t.Weight,
}
}
if err := a.streamSrv.AddUpstream(sc.Listen, targets, sc.Upstream.LoadBalance, stream.HealthCheckSpec{}); err != nil {
a.logger.Error().Err(err).Msg("Failed to add Stream upstream")
}
if sc.Protocol == "udp" {
if err := a.streamSrv.ListenUDP(sc.Listen, sc.Listen, 60*time.Second); err != nil {
a.logger.Error().Err(err).Str("listen", sc.Listen).Msg("Failed to listen on UDP")
}
} else {
if err := a.streamSrv.ListenTCP(sc.Listen); err != nil {
a.logger.Error().Err(err).Str("listen", sc.Listen).Msg("Failed to listen on TCP")
}
}
}
go func() {
a.logger.LogStartup("Starting Stream server", nil)
if err := a.streamSrv.Start(); err != nil {
a.logger.Error().Err(err).Msg("Stream server failed to start")
}
}()
}
// initHTTP3 starts the HTTP/3 server if enabled.
func (a *App) initHTTP3() {
if len(a.cfg.Servers) == 0 || !a.cfg.HTTP3.Enabled || a.cfg.Servers[0].SSL.Cert == "" {
return
}
tlsConfig, err := a.srv.GetTLSConfig()
if err != nil {
a.logger.Error().Err(err).Msg("Failed to get TLS config, skipping HTTP/3")
return
}
a.http3Srv, err = http3.NewServer(&a.cfg.HTTP3, a.srv.GetHandler(), tlsConfig)
if err != nil {
a.logger.Error().Err(err).Msg("Failed to create HTTP/3 server")
return
}
go func() {
a.logger.LogStartup("Starting HTTP/3 server", map[string]string{"listen": a.cfg.HTTP3.Listen})
if err := a.http3Srv.Start(); err != nil {
a.logger.Error().Err(err).Msg("HTTP/3 server failed to start")
}
}()
}
// initHTTP2 starts the HTTP/2 server if enabled.
func (a *App) initHTTP2() {
if len(a.cfg.Servers) == 0 || !a.cfg.Servers[0].SSL.HTTP2.Enabled || a.cfg.Servers[0].SSL.Cert == "" {
return
}
tlsConfig, err := a.srv.GetTLSConfig()
if err != nil {
a.logger.Error().Err(err).Msg("Failed to get TLS config, skipping HTTP/2")
return
}
a.http2Srv, err = http2.NewServer(&a.cfg.Servers[0].SSL.HTTP2, a.srv.GetHandler(), tlsConfig)
if err != nil {
a.logger.Error().Err(err).Msg("Failed to create HTTP/2 server")
return
}
go func() {
a.logger.LogStartup("Starting HTTP/2 server", map[string]string{
"listen": a.cfg.Servers[0].Listen,
"max_concurrent_streams": fmt.Sprintf("%d", a.cfg.Servers[0].SSL.HTTP2.MaxConcurrentStreams),
"push_enabled": fmt.Sprintf("%t", a.cfg.Servers[0].SSL.HTTP2.PushEnabled),
})
// HTTP/2 shares the main server's listener; ALPN negotiates protocol selection.
listeners := a.srv.GetListeners()
if len(listeners) > 0 {
if err := a.http2Srv.Serve(listeners[0]); err != nil {
a.logger.Error().Err(err).Msg("HTTP/2 server failed to start")
}
} else {
a.logger.Error().Msg("HTTP/2 server failed to start: no available listeners")
}
}()
}
// shutdownHTTP3 gracefully stops the HTTP/3 server.
func (a *App) shutdownHTTP3() {
if a.http3Srv != nil {
if err := a.http3Srv.Stop(); err != nil {
a.logger.Error().Err(err).Msg("Failed to shutdown HTTP/3 server")
}
}
}
// shutdownHTTP2 gracefully stops the HTTP/2 server.
func (a *App) shutdownHTTP2() {
if a.http2Srv != nil {
if err := a.http2Srv.Stop(); err != nil {
a.logger.Error().Err(err).Msg("Failed to shutdown HTTP/2 server")
}
}
}
// reopenLogs reinitializes the logger from current config.
func (a *App) shutdownStream() {
if a.streamSrv != nil {
a.streamSrv.Stop()
}
}
func (a *App) reopenLogs() {
if a.cfg != nil {
logging.Init(a.cfg.Logging.Error.Level, a.cfg.Logging.Format)
a.logger = logging.NewAppLogger(&a.cfg.Logging)
}
a.logger.LogStartup("Logs reopened", nil)
}