/* * Copyright (C) 2020-2022, IrineSistiana * * This file is part of mosdns. * * mosdns is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * mosdns is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package coremain import ( "bytes" "errors" "fmt" "io" "net/http" "net/http/pprof" "github.com/IrineSistiana/mosdns/v5/mlog" "github.com/IrineSistiana/mosdns/v5/pkg/safe_close" "github.com/go-chi/chi/v5" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/collectors" "github.com/prometheus/client_golang/prometheus/promhttp" "go.uber.org/zap" ) type Mosdns struct { logger *zap.Logger // non-nil logger. // Plugins plugins map[string]any httpMux *chi.Mux // API 路由 webMux *chi.Mux // Web UI 路由(独立) metricsReg *prometheus.Registry sc *safe_close.SafeClose } // NewMosdns initializes a mosdns instance and its plugins. func NewMosdns(cfg *Config) (*Mosdns, error) { // Init logger. lg, err := mlog.NewLogger(cfg.Log) if err != nil { return nil, fmt.Errorf("failed to init logger: %w", err) } m := &Mosdns{ logger: lg, plugins: make(map[string]any), httpMux: chi.NewRouter(), // API 路由 webMux: chi.NewRouter(), // Web UI 独立路由 metricsReg: newMetricsReg(), sc: safe_close.NewSafeClose(), } // This must be called after m.httpMux and m.metricsReg been set. m.initHttpMux() // Register management API routes m.registerManagementAPI() // 如果配置了独立的 Web 端口,初始化 Web 路由 if len(cfg.Web.HTTP) > 0 { m.initWebMux() m.registerWebUI() m.registerWebAPI() } // Start http api server if httpAddr := cfg.API.HTTP; len(httpAddr) > 0 { // 设置当前 API 地址,供 Web UI 显示 SetCurrentAPIAddress(httpAddr) httpServer := &http.Server{ Addr: httpAddr, Handler: m.httpMux, } m.sc.Attach(func(done func(), closeSignal <-chan struct{}) { defer done() errChan := make(chan error, 1) go func() { m.logger.Info("starting api http server", zap.String("addr", httpAddr)) errChan <- httpServer.ListenAndServe() }() select { case err := <-errChan: m.sc.SendCloseSignal(err) case <-closeSignal: _ = httpServer.Close() } }) } // Start web ui server (独立端口) if webAddr := cfg.Web.HTTP; len(webAddr) > 0 { webServer := &http.Server{ Addr: webAddr, Handler: m.webMux, } m.sc.Attach(func(done func(), closeSignal <-chan struct{}) { defer done() errChan := make(chan error, 1) go func() { m.logger.Info("starting web ui http server", zap.String("addr", webAddr)) errChan <- webServer.ListenAndServe() }() select { case err := <-errChan: m.sc.SendCloseSignal(err) case <-closeSignal: _ = webServer.Close() } }) } // Load plugins. // Close all plugins on signal. // From here, call m.sc.SendCloseSignal() if any plugin failed to load. m.sc.Attach(func(done func(), closeSignal <-chan struct{}) { go func() { defer done() <-closeSignal m.logger.Info("starting shutdown sequences") for tag, p := range m.plugins { if closer, _ := p.(io.Closer); closer != nil { m.logger.Info("closing plugin", zap.String("tag", tag)) _ = closer.Close() } } m.logger.Info("all plugins were closed") }() }) // Preset plugins if err := m.loadPresetPlugins(); err != nil { m.sc.SendCloseSignal(err) _ = m.sc.WaitClosed() return nil, err } // Plugins from config. if err := m.loadPluginsFromCfg(cfg, 0); err != nil { m.sc.SendCloseSignal(err) _ = m.sc.WaitClosed() return nil, err } m.logger.Info("all plugins are loaded") return m, nil } // NewTestMosdnsWithPlugins returns a mosdns instance for testing. func NewTestMosdnsWithPlugins(p map[string]any) *Mosdns { return &Mosdns{ logger: mlog.Nop(), httpMux: chi.NewRouter(), plugins: p, metricsReg: newMetricsReg(), sc: safe_close.NewSafeClose(), } } func (m *Mosdns) GetSafeClose() *safe_close.SafeClose { return m.sc } // CloseWithErr is a shortcut for m.sc.SendCloseSignal func (m *Mosdns) CloseWithErr(err error) { m.sc.SendCloseSignal(err) } // Logger returns a non-nil logger. func (m *Mosdns) Logger() *zap.Logger { return m.logger } // GetPlugin returns a plugin. func (m *Mosdns) GetPlugin(tag string) any { return m.plugins[tag] } // GetMetricsReg returns a prometheus.Registerer with a prefix of "mosdns_" func (m *Mosdns) GetMetricsReg() prometheus.Registerer { return prometheus.WrapRegistererWithPrefix("mosdns_", m.metricsReg) } func (m *Mosdns) GetAPIRouter() *chi.Mux { return m.httpMux } func (m *Mosdns) RegPluginAPI(tag string, mux *chi.Mux) { m.httpMux.Mount("/plugins/"+tag, mux) } func newMetricsReg() *prometheus.Registry { reg := prometheus.NewRegistry() reg.MustRegister(collectors.NewProcessCollector(collectors.ProcessCollectorOpts{})) reg.MustRegister(collectors.NewGoCollector()) return reg } // initHttpMux initializes api entries. It MUST be called after m.metricsReg being initialized. func (m *Mosdns) initHttpMux() { // Register metrics. m.httpMux.Method(http.MethodGet, "/metrics", promhttp.HandlerFor(m.metricsReg, promhttp.HandlerOpts{})) // Register pprof. m.httpMux.Route("/debug/pprof", func(r chi.Router) { r.Get("/*", pprof.Index) r.Get("/cmdline", pprof.Cmdline) r.Get("/profile", pprof.Profile) r.Get("/symbol", pprof.Symbol) r.Get("/trace", pprof.Trace) }) // A helper page for invalid request. invalidApiReqHelper := func(w http.ResponseWriter, req *http.Request) { b := new(bytes.Buffer) _, _ = fmt.Fprintf(b, "Invalid request %s %s\n\n", req.Method, req.RequestURI) b.WriteString("Available api urls:\n") _ = chi.Walk(m.httpMux, func(method string, route string, handler http.Handler, middlewares ...func(http.Handler) http.Handler) error { b.WriteString(method) b.WriteByte(' ') b.WriteString(route) b.WriteByte('\n') return nil }) _, _ = w.Write(b.Bytes()) } m.httpMux.NotFound(invalidApiReqHelper) m.httpMux.MethodNotAllowed(invalidApiReqHelper) } // initWebMux 初始化 Web UI 路由 func (m *Mosdns) initWebMux() { // Web UI 的 404 页面 m.webMux.NotFound(func(w http.ResponseWriter, req *http.Request) { http.Error(w, "Page not found", http.StatusNotFound) }) } func (m *Mosdns) loadPresetPlugins() error { for tag, f := range LoadNewPersetPluginFuncs() { p, err := f(NewBP(tag, m)) if err != nil { return fmt.Errorf("failed to init preset plugin %s, %w", tag, err) } m.plugins[tag] = p } return nil } // loadPluginsFromCfg loads plugins from this config. It follows include first. func (m *Mosdns) loadPluginsFromCfg(cfg *Config, includeDepth int) error { const maxIncludeDepth = 8 if includeDepth > maxIncludeDepth { return errors.New("maximum include depth reached") } includeDepth++ // Follow include first. for _, s := range cfg.Include { subCfg, path, err := loadConfig(s) if err != nil { return fmt.Errorf("failed to read config from %s, %w", s, err) } m.logger.Info("load config", zap.String("file", path)) if err := m.loadPluginsFromCfg(subCfg, includeDepth); err != nil { return fmt.Errorf("failed to load config from %s, %w", s, err) } } for i, pc := range cfg.Plugins { if err := m.newPlugin(pc); err != nil { return fmt.Errorf("failed to init plugin #%d %s, %w", i, pc.Tag, err) } } return nil }