package httpserver import ( "context" "crypto/tls" "net" "net/http" "sync" intlog "log" "github.com/flosch/pongo2/v4" "github.com/gin-gonic/gin" "github.com/rs/zerolog/log" "go.sebtobie.de/httpserver/auth" "go.sebtobie.de/httpserver/constants" "go.sebtobie.de/httpserver/funcs" "go.sebtobie.de/httpserver/menus" "go.sebtobie.de/httpserver/middleware" "go.sebtobie.de/httpserver/templates" ) func init() { gin.DebugPrintRouteFunc = func(httpMethod, absolutePath, handlerName string, nuHandlers int) { log.Debug().Msgf("%-4s(%02d): %-20s %s", httpMethod, nuHandlers, absolutePath, handlerName) } gin.SetMode(gin.DebugMode) } // Config that is used to map the toml config to the settings that are used. type Config struct { Addr []string TLSAddr []string TLSconfig *tls.Config `toml:"-"` Certfile string Keyfile string Sites map[string]SiteConfig Middleware map[string]middleware.Config } /** // MarshalObject adds the information over the object to the *log.Entry func (c *Config) MarshalObject(e *log.Entry) { e.Strs("Address", c.Addr).Bool("TLS", c.TLSconfig != nil) if c.TLSconfig != nil { e.Str("Certfile", c.Certfile) e.Str("Keyfile", c.Keyfile) } e.Int("sites", len(c.Sites)) } var _ log.ObjectMarshaler = &Config{} /**/ // Server is an wrapper for the *http.Server and *gin.Engine type Server struct { http *http.Server Conf *Config mrouter map[string]*gin.Engine sites map[string]Site menu []menus.Menu template *pongo2.TemplateSet NotFoundHandler http.Handler routines sync.WaitGroup setup bool authh auth.AuthenticationHandler middleware gin.HandlersChain advmiddleware map[string]middleware.Middleware } // CreateServer creates an server that can be run in a coroutine. func CreateServer() *Server { log.Info().Msg("Redirect logging output to phuslu/log") gin.DefaultErrorWriter = log.Logger.With().Str("source", "GIN").Logger() gin.DefaultWriter = log.Logger.With().Str("source", "GIN").Logger() log.Info().Msg("Creating HTTP-Server") var server = &Server{ Conf: &Config{ TLSconfig: &tls.Config{}, Sites: map[string]SiteConfig{}, }, mrouter: map[string]*gin.Engine{}, authh: &auth.AnonAccountHandler{}, menu: []menus.Menu{}, NotFoundHandler: http.NotFoundHandler(), sites: map[string]Site{}, middleware: gin.HandlersChain{}, template: pongo2.NewSet("templates", &templates.EmptyLoader{}), } server.http = &http.Server{ ErrorLog: intlog.New(log.Logger, "", 0), Handler: http.HandlerFunc(server.DomainRouter), } return server } // runPort runs a listener on the port. his enables th server to serve more than a address. func (s *Server) runPort(address string, tls bool) { defer s.routines.Done() var socket net.Listener var err error var unix string if funcs.IsTCP(address) { socket, err = net.Listen("tcp", address) } if funcs.IsUnix(address) { unix = "Unix-" socket, err = net.Listen("unix", address) } if err != nil { log.Error().Err(err).Msgf("failed to open socket on %s", address) return } if socket == nil { log.Error().Msg("Failed to identify the sockettype") return } if tls { log.Info().Msgf("starting listen on secure %ssocket %s", unix, address) err = s.http.ServeTLS(socket, s.Conf.Certfile, s.Conf.Keyfile) } else { log.Info().Msgf("starting listen on %ssocket %s", unix, address) err = s.http.Serve(socket) } if err != http.ErrServerClosed { log.Error().Err(err).Msg("Socket unexpected exited") } } // SetAuthentication sets the handler that is responsible for authentication func (s *Server) SetAuthentication(a auth.AuthenticationHandler) { s.authh = a } // StartServer starts the server as configured and sends the errormessage to the log. // it blocks until all ports are closed. func (s *Server) StartServer() { if !s.setup { log.Error().Msg("Server not set up") return } log.Info().Msg("Starting server") s.http.TLSConfig = s.Conf.TLSconfig if s.Conf.Certfile != "" && s.Conf.Keyfile != "" { for _, addr := range s.Conf.TLSAddr { s.routines.Add(1) go s.runPort(addr, true) } } for _, addr := range s.Conf.Addr { s.routines.Add(1) go s.runPort(addr, false) } s.routines.Wait() } // DomainRouter redirects the requests to the routers of the domains func (s *Server) DomainRouter(w http.ResponseWriter, r *http.Request) { var domain string if r.URL.Host != "" { domain = r.URL.Host } else if r.Host != "" { domain = r.Host } else if r.Header.Get("X-Original-Host") != "" { domain = r.Header.Get("X-Original-Host") } r.Host = domain r.URL.Host = domain for header, value := range map[string][]string(r.Header) { log.Trace().Strs(header, value).Msg("Headers") } if router, found := s.mrouter[domain]; found { router.NoMethod(gin.WrapH(s.NotFoundHandler)) router.NoRoute(gin.WrapH(s.NotFoundHandler)) router.ServeHTTP(w, r) return } log.Error().Msgf("Failed to find domain for %s", domain) var entrys []string for d := range s.mrouter { entrys = append(entrys, d) } log.Trace().Strs("registred domains", entrys).Msg("domain not found") s.NotFoundHandler.ServeHTTP(w, r) } // Use installs the middleware into the router. // The Middleware must be able to detect multiple calls byy itself. Deduplication is not performed. func (s *Server) Use(m ...gin.HandlerFunc) { s.middleware = append(s.middleware, m...) for _, site := range s.mrouter { site.Use(m...) } } // Stop Shuts the Server down func (s *Server) Stop(ctx context.Context) { log.Info().Err(s.http.Shutdown(ctx)).Msg("Server Shut down.") for _, s := range s.sites { if ts, ok := s.(TeardownSite); ok { ts.Teardown() } } } func (s *Server) menus() []menus.Menu { return s.menu } func maptoarray(m map[string]Site) (a []interface{}) { for _, i := range m { a = append(a, i) } return } // Setup sets the server up. It loads the sites and prepare the server for startup. // The Midleware and the site are setup in this Order: // 1. Middleware.Setup // 2. Site.Init // 3. Middleware.Sites // 4. Site.Setup func (s *Server) Setup() { log.Info().Msg("Perparing server for start") var ( router *gin.Engine found bool site Site cfg string ) for cfg, middleware := range s.advmiddleware { middleware.Setup(s.Conf.Middleware[cfg]) } for cfg, site = range s.sites { config := s.Conf.Sites[cfg] if router, found = s.mrouter[config["domain"].(string)]; !found { log.Info().Msgf("Setting up router for %s", config["domain"].(string)) router = gin.New() router.Use(func(c *gin.Context) { c.Set(constants.Domain, config["domain"]) c.Set(constants.Menus, s.menus) c.Set(constants.Accounts, s.authh.Account(c)) }) router.Use(s.middleware...) router.HTMLRender = templates.NewPongo2Renderer(s.template) s.mrouter[config["domain"].(string)] = router } group := router.Group(config["path"].(string)) site.Init(group) } for _, middleware := range s.advmiddleware { if err := middleware.Sites(maptoarray(s.sites)); err != nil { log.Error().Err(err).Msg("Failed to setup midddleware. Stopping with the setup") return } } for cfg, site = range s.sites { config := s.Conf.Sites[cfg] if ms, ok := site.(menus.MenuSite); ok { menus := ms.Menu(config["domain"].(string)) log.Debug().Msgf("%d menus are added", len(menus)) s.menu = append(s.menu, menus...) } if ts, ok := site.(templates.TemplateSite); ok { templates := ts.Templates() if templates == nil { log.Error().Msgf("Site %s had an empty templateloader", cfg) } else { s.template.AddLoader(templates) } } if cs, ok := site.(ConfigSite); ok { cs.Setup(config) } } s.setup = true } // RegisterSite adds an site to the engine as its own grouo // it registers the defaults so that the application can load/dump it from/into an configfile or commandline options func (s *Server) RegisterSite(cfg string, site Site) { if cs, ok := site.(ConfigSite); ok { var config = cs.Defaults() if _, found := config["domain"]; !found { config["domain"] = "" } if _, found := config["path"]; !found { config["path"] = "" } s.Conf.Sites[cfg] = config } s.sites[cfg] = site } // RegisterMiddleware registers middleware that has avanced functions, like persistence. // That middleware allows configuration with the Setup Method and the Teardown allows an safe method for closing connections and shutting down. // Middleware will be set up before sites. func (s *Server) RegisterMiddleware(cfg string, m middleware.Middleware) { s.Conf.Middleware[cfg] = m.Defaults() s.Use(m.Gin) s.advmiddleware[cfg] = m }