mosdns/coremain/mosdns.go
dengxiongjian ee06785e08
Some checks are pending
Test mosdns / build (push) Waiting to run
开发web管理
2025-10-15 22:20:27 +08:00

291 lines
7.8 KiB
Go

/*
* 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 <https://www.gnu.org/licenses/>.
*/
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
}