1
0
mirror of https://github.com/jhillyerd/inbucket.git synced 2025-12-17 17:47:03 +00:00
Files
go-inbucket/pkg/server/web/server.go
2018-03-27 20:26:06 -07:00

172 lines
4.6 KiB
Go

// 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)
}
}