mirror of
https://github.com/jhillyerd/inbucket.git
synced 2025-12-17 17:47:03 +00:00
- rest: improve error logging. - web: extract handlers/middleware into their own file. - web: log all requests, not just ones hitting our handlers. - test: improve integration test logging format.
157 lines
4.3 KiB
Go
157 lines
4.3 KiB
Go
// Package web provides the plumbing for Inbucket's web GUI and RESTful API
|
|
package web
|
|
|
|
import (
|
|
"context"
|
|
"expvar"
|
|
"net"
|
|
"net/http"
|
|
"net/http/pprof"
|
|
"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"
|
|
)
|
|
|
|
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
|
|
log.Info().Str("module", "web").Str("phase", "startup").Str("path", conf.Web.UIDir).
|
|
Msg("Web UI content mapped")
|
|
Router.Handle("/debug/vars", expvar.Handler())
|
|
if conf.Web.PProf {
|
|
Router.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
|
|
Router.HandleFunc("/debug/pprof/profile", pprof.Profile)
|
|
Router.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
|
|
Router.HandleFunc("/debug/pprof/trace", pprof.Trace)
|
|
Router.PathPrefix("/debug/pprof/").HandlerFunc(pprof.Index)
|
|
log.Warn().Str("module", "web").Str("phase", "startup").
|
|
Msg("Go pprof tools installed to /debug/pprof")
|
|
}
|
|
// If no other route matches, attempt to service as UI element.
|
|
Router.PathPrefix("/").Handler(http.StripPrefix("/", http.FileServer(http.Dir(conf.Web.UIDir))))
|
|
Router.NotFoundHandler = noMatchHandler(http.StatusNotFound, "No route matches URI path")
|
|
Router.MethodNotAllowedHandler = noMatchHandler(http.StatusMethodNotAllowed,
|
|
"Method not allowed for URI path")
|
|
|
|
// 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: requestLoggingWrapper(Router),
|
|
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
|
|
}
|
|
}
|
|
|
|
func emergencyShutdown() {
|
|
// Shutdown Inbucket
|
|
select {
|
|
case _ = <-globalShutdown:
|
|
default:
|
|
close(globalShutdown)
|
|
}
|
|
}
|