mirror of
https://github.com/jhillyerd/inbucket.git
synced 2025-12-17 17:47:03 +00:00
* fix: POP3 server now uses TLS HandshakeContext Signed-off-by: James Hillyerd <james@hillyerd.com> * fix: Web server now uses Listen with context Signed-off-by: James Hillyerd <james@hillyerd.com> * fix: replace interface{} with any Signed-off-by: James Hillyerd <james@hillyerd.com> --------- Signed-off-by: James Hillyerd <james@hillyerd.com>
215 lines
6.2 KiB
Go
215 lines
6.2 KiB
Go
// Package web provides the plumbing for Inbucket's web GUI and RESTful API
|
|
package web
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"expvar"
|
|
"html/template"
|
|
"net"
|
|
"net/http"
|
|
"net/http/pprof"
|
|
"net/url"
|
|
"os"
|
|
"path/filepath"
|
|
"time"
|
|
|
|
"github.com/gorilla/mux"
|
|
"github.com/inbucket/inbucket/v3/pkg/config"
|
|
"github.com/inbucket/inbucket/v3/pkg/message"
|
|
"github.com/inbucket/inbucket/v3/pkg/msghub"
|
|
"github.com/inbucket/inbucket/v3/pkg/stringutil"
|
|
"github.com/rs/zerolog/log"
|
|
)
|
|
|
|
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
|
|
|
|
// ExpWebSocketConnectsCurrent tracks the number of open WebSockets
|
|
ExpWebSocketConnectsCurrent = new(expvar.Int)
|
|
)
|
|
|
|
func init() {
|
|
m := expvar.NewMap("http")
|
|
m.Set("WebSocketConnectsCurrent", ExpWebSocketConnectsCurrent)
|
|
}
|
|
|
|
// Server defines an instance of the Web server.
|
|
type Server struct {
|
|
// TODO Migrate global vars here.
|
|
notify chan error // Notify on fatal error.
|
|
}
|
|
|
|
// NewServer sets up things for unit tests or the Start() method.
|
|
func NewServer(conf *config.Root, mm message.Manager, mh *msghub.Hub) *Server {
|
|
rootConfig = conf
|
|
|
|
// NewContext() will use this DataStore for the web handlers.
|
|
msgHub = mh
|
|
manager = mm
|
|
|
|
// Redirect requests to / if there is a base path configured.
|
|
prefix := stringutil.MakePathPrefixer(conf.Web.BasePath)
|
|
redirectBase := prefix("/")
|
|
if redirectBase != "/" {
|
|
log.Info().Str("module", "web").Str("phase", "startup").Str("path", redirectBase).
|
|
Msg("Base path configured")
|
|
Router.Path("/").Handler(http.RedirectHandler(redirectBase, http.StatusFound))
|
|
|
|
// Redirect prefix when missing trailing slash.
|
|
Router.Path(prefix("")).Handler(http.RedirectHandler(redirectBase, http.StatusFound))
|
|
}
|
|
|
|
// Dynamic paths.
|
|
log.Info().Str("module", "web").Str("phase", "startup").Str("path", conf.Web.UIDir).
|
|
Msg("Web UI content mapped")
|
|
Router.Handle(prefix("/debug/vars"), expvar.Handler())
|
|
if conf.Web.PProf {
|
|
Router.HandleFunc(prefix("/debug/pprof/cmdline"), pprof.Cmdline)
|
|
Router.HandleFunc(prefix("/debug/pprof/profile"), pprof.Profile)
|
|
Router.HandleFunc(prefix("/debug/pprof/symbol"), pprof.Symbol)
|
|
Router.HandleFunc(prefix("/debug/pprof/trace"), pprof.Trace)
|
|
Router.PathPrefix(prefix("/debug/pprof/")).HandlerFunc(pprof.Index)
|
|
log.Warn().Str("module", "web").Str("phase", "startup").
|
|
Msg("Go pprof tools installed to " + prefix("/debug/pprof"))
|
|
}
|
|
|
|
// Static paths.
|
|
Router.PathPrefix(prefix("/static")).Handler(
|
|
http.StripPrefix(prefix("/"), http.FileServer(http.Dir(conf.Web.UIDir))))
|
|
Router.Path(prefix("/favicon.png")).Handler(
|
|
fileHandler(filepath.Join(conf.Web.UIDir, "favicon.png")))
|
|
|
|
// Parse index.html template, allowing for configuration to be passed to the SPA.
|
|
indexPath := filepath.Join(conf.Web.UIDir, "index.html")
|
|
indexTmpl, err := template.ParseFiles(indexPath)
|
|
if err != nil {
|
|
msg := "Failed to parse HTML template"
|
|
cwd, _ := os.Getwd()
|
|
log.Error().
|
|
Str("module", "web").
|
|
Str("phase", "startup").
|
|
Str("path", indexPath).
|
|
Str("cwd", cwd).
|
|
Err(err).
|
|
Msg(msg)
|
|
// Create a dummy template to allow tests to pass.
|
|
indexTmpl, _ = template.New("index.html").Parse(msg)
|
|
}
|
|
|
|
// SPA managed paths.
|
|
spaHandler := cookieHandler(appConfigCookie(conf.Web),
|
|
spaTemplateHandler(indexTmpl, prefix("/")))
|
|
Router.Path(prefix("/")).Handler(spaHandler)
|
|
Router.Path(prefix("/monitor")).Handler(spaHandler)
|
|
Router.Path(prefix("/status")).Handler(spaHandler)
|
|
Router.PathPrefix(prefix("/m/")).Handler(spaHandler)
|
|
|
|
// Error handlers.
|
|
Router.NotFoundHandler = noMatchHandler(
|
|
http.StatusNotFound, "No route matches URI path")
|
|
Router.MethodNotAllowedHandler = noMatchHandler(
|
|
http.StatusMethodNotAllowed, "Method not allowed for URI path")
|
|
|
|
s := &Server{
|
|
notify: make(chan error, 1),
|
|
}
|
|
|
|
return s
|
|
}
|
|
|
|
// Start begins listening for HTTP requests
|
|
func (s *Server) Start(ctx context.Context, readyFunc func()) {
|
|
var (
|
|
err error
|
|
listenCfg net.ListenConfig
|
|
)
|
|
|
|
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")
|
|
|
|
// This context is only used while the listener is resolving our address.
|
|
listenCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
|
|
defer cancel()
|
|
listener, err = listenCfg.Listen(listenCtx, "tcp", server.Addr)
|
|
if err != nil {
|
|
log.Error().Str("module", "web").Str("phase", "startup").Err(err).
|
|
Msg("HTTP failed to start TCP4 listener")
|
|
s.notify <- err
|
|
close(s.notify)
|
|
return
|
|
}
|
|
|
|
// Start listener go routine
|
|
go s.serve(ctx)
|
|
readyFunc()
|
|
|
|
// Wait for shutdown
|
|
<-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")
|
|
}
|
|
}
|
|
|
|
func appConfigCookie(webConfig config.Web) *http.Cookie {
|
|
o := &jsonAppConfig{
|
|
BasePath: webConfig.BasePath,
|
|
MonitorVisible: webConfig.MonitorVisible,
|
|
}
|
|
b, err := json.Marshal(o)
|
|
if err != nil {
|
|
log.Error().Str("module", "web").Str("phase", "startup").Err(err).
|
|
Msg("Failed to convert app-config to JSON")
|
|
}
|
|
return &http.Cookie{
|
|
Name: "app-config",
|
|
Value: url.PathEscape(string(b)),
|
|
Path: "/",
|
|
}
|
|
}
|
|
|
|
// serve begins serving HTTP requests
|
|
func (s *Server) 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")
|
|
s.notify <- err
|
|
close(s.notify)
|
|
return
|
|
}
|
|
}
|
|
|
|
// Notify allows the running Web server to be monitored for a fatal error.
|
|
func (s *Server) Notify() <-chan error {
|
|
return s.notify
|
|
}
|