// Package web provides the plumbing for Inbucket's web GUI and RESTful API package web import ( "context" "expvar" "net" "net/http" "path/filepath" "time" "github.com/gorilla/mux" "github.com/gorilla/securecookie" "github.com/gorilla/sessions" "github.com/jhillyerd/inbucket/pkg/config" "github.com/jhillyerd/inbucket/pkg/message" "github.com/jhillyerd/inbucket/pkg/msghub" "github.com/rs/zerolog/log" ) // Handler is a function type that handles an HTTP request in Inbucket type Handler func(http.ResponseWriter, *http.Request, *Context) error const ( staticDir = "static" templateDir = "templates" ) var ( // msgHub holds a reference to the message pub/sub system msgHub *msghub.Hub manager message.Manager // Router is shared between httpd, webui and rest packages. It sends // incoming requests to the correct handler function Router = mux.NewRouter() rootConfig *config.Root server *http.Server listener net.Listener sessionStore sessions.Store globalShutdown chan bool // ExpWebSocketConnectsCurrent tracks the number of open WebSockets ExpWebSocketConnectsCurrent = new(expvar.Int) ) func init() { m := expvar.NewMap("http") m.Set("WebSocketConnectsCurrent", ExpWebSocketConnectsCurrent) } // Initialize sets up things for unit tests or the Start() method func Initialize( conf *config.Root, shutdownChan chan bool, mm message.Manager, mh *msghub.Hub) { rootConfig = conf globalShutdown = shutdownChan // NewContext() will use this DataStore for the web handlers msgHub = mh manager = mm // Content Paths staticPath := filepath.Join(conf.Web.UIDir, staticDir) log.Info().Str("module", "web").Str("phase", "startup").Str("path", conf.Web.UIDir). Msg("Web UI content mapped") Router.PathPrefix("/public/").Handler(http.StripPrefix("/public/", http.FileServer(http.Dir(staticPath)))) http.Handle("/", Router) // Session cookie setup if conf.Web.CookieAuthKey == "" { log.Info().Str("module", "web").Str("phase", "startup"). Msg("Generating random cookie.auth.key") sessionStore = sessions.NewCookieStore(securecookie.GenerateRandomKey(64)) } else { log.Info().Str("module", "web").Str("phase", "startup"). Msg("Using configured cookie.auth.key") sessionStore = sessions.NewCookieStore([]byte(conf.Web.CookieAuthKey)) } } // Start begins listening for HTTP requests func Start(ctx context.Context) { server = &http.Server{ Addr: rootConfig.Web.Addr, Handler: nil, ReadTimeout: 60 * time.Second, WriteTimeout: 60 * time.Second, } // We don't use ListenAndServe because it lacks a way to close the listener log.Info().Str("module", "web").Str("phase", "startup").Str("addr", server.Addr). Msg("HTTP listening on tcp4") var err error listener, err = net.Listen("tcp", server.Addr) if err != nil { log.Error().Str("module", "web").Str("phase", "startup").Err(err). Msg("HTTP failed to start TCP4 listener") emergencyShutdown() return } // Listener go routine go serve(ctx) // Wait for shutdown select { case _ = <-ctx.Done(): log.Debug().Str("module", "web").Str("phase", "shutdown"). Msg("HTTP server shutting down on request") } // Closing the listener will cause the serve() go routine to exit if err := listener.Close(); err != nil { log.Debug().Str("module", "web").Str("phase", "shutdown").Err(err). Msg("Failed to close HTTP listener") } } // serve begins serving HTTP requests func serve(ctx context.Context) { // server.Serve blocks until we close the listener err := server.Serve(listener) select { case _ = <-ctx.Done(): // Nop default: log.Error().Str("module", "web").Str("phase", "startup").Err(err). Msg("HTTP server failed") emergencyShutdown() return } } // ServeHTTP builds the context and passes onto the real handler func (h Handler) ServeHTTP(w http.ResponseWriter, req *http.Request) { // Create the context ctx, err := NewContext(req) if err != nil { log.Error().Str("module", "web").Err(err).Msg("HTTP failed to create context") http.Error(w, err.Error(), http.StatusInternalServerError) return } defer ctx.Close() // Run the handler, grab the error, and report it log.Debug().Str("module", "web").Str("remote", req.RemoteAddr).Str("proto", req.Proto). Str("method", req.Method).Str("path", req.RequestURI).Msg("Request") err = h(w, req, ctx) if err != nil { log.Error().Str("module", "web").Str("path", req.RequestURI).Err(err). Msg("Error handling request") http.Error(w, err.Error(), http.StatusInternalServerError) return } } func emergencyShutdown() { // Shutdown Inbucket select { case _ = <-globalShutdown: default: close(globalShutdown) } }