2023-05-31 12:49:45 +00:00
|
|
|
package webserver
|
2021-11-19 18:37:42 +00:00
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
2022-12-07 10:48:23 +00:00
|
|
|
crand "crypto/rand"
|
|
|
|
"encoding/base64"
|
2024-03-14 12:33:52 +00:00
|
|
|
"encoding/json"
|
2021-11-19 18:37:42 +00:00
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"io/fs"
|
2024-03-14 12:33:52 +00:00
|
|
|
"log/slog"
|
2022-12-07 10:48:23 +00:00
|
|
|
mrand "math/rand"
|
2021-11-19 18:37:42 +00:00
|
|
|
"net/http"
|
2022-03-24 09:16:51 +00:00
|
|
|
"net/http/pprof"
|
2024-02-25 13:46:59 +00:00
|
|
|
"time"
|
2021-11-19 18:37:42 +00:00
|
|
|
|
2023-11-10 20:36:15 +00:00
|
|
|
"git.sr.ht/~ashkeel/containers/sync"
|
2024-03-14 12:33:52 +00:00
|
|
|
kv "git.sr.ht/~ashkeel/kilovolt/v12"
|
2023-11-10 20:36:15 +00:00
|
|
|
"git.sr.ht/~ashkeel/strimertul/database"
|
2021-11-19 18:37:42 +00:00
|
|
|
)
|
|
|
|
|
2023-05-31 12:49:45 +00:00
|
|
|
type WebServer struct {
|
2022-12-04 13:45:34 +00:00
|
|
|
Config *sync.RWSync[ServerConfig]
|
2024-03-10 16:38:18 +00:00
|
|
|
db database.Database
|
2023-05-31 12:49:45 +00:00
|
|
|
server Server
|
2022-11-30 18:15:47 +00:00
|
|
|
frontend fs.FS
|
|
|
|
hub *kv.Hub
|
|
|
|
mux *http.ServeMux
|
2022-12-04 13:45:34 +00:00
|
|
|
requestedRoutes *sync.Map[string, http.Handler]
|
2023-05-05 13:02:23 +00:00
|
|
|
restart *sync.RWSync[bool]
|
2022-12-03 15:16:59 +00:00
|
|
|
cancelConfigSub database.CancelFunc
|
2023-05-31 12:49:45 +00:00
|
|
|
factory ServerFactory
|
2021-11-19 18:37:42 +00:00
|
|
|
}
|
|
|
|
|
2024-03-14 12:33:52 +00:00
|
|
|
func NewServer(db database.Database, serverFactory ServerFactory) (*WebServer, error) {
|
2023-05-31 12:49:45 +00:00
|
|
|
server := &WebServer{
|
2022-11-30 18:15:47 +00:00
|
|
|
db: db,
|
2023-05-31 12:49:45 +00:00
|
|
|
server: nil,
|
2022-12-04 13:45:34 +00:00
|
|
|
requestedRoutes: sync.NewMap[string, http.Handler](),
|
2023-05-05 13:02:23 +00:00
|
|
|
restart: sync.NewRWSync(false),
|
2022-12-04 13:45:34 +00:00
|
|
|
Config: sync.NewRWSync(ServerConfig{}),
|
2023-05-31 12:49:45 +00:00
|
|
|
factory: serverFactory,
|
2021-11-19 18:37:42 +00:00
|
|
|
}
|
2022-11-30 18:15:47 +00:00
|
|
|
|
2022-12-04 17:34:59 +00:00
|
|
|
var config ServerConfig
|
|
|
|
err := db.GetJSON(ServerConfigKey, &config)
|
2021-11-21 21:36:48 +00:00
|
|
|
if err != nil {
|
2024-02-25 13:46:59 +00:00
|
|
|
if !errors.Is(err, database.ErrEmptyKey) {
|
2024-03-14 12:33:52 +00:00
|
|
|
slog.Warn("HTTP config is corrupted or could not be read", "error", err)
|
2022-12-04 17:34:59 +00:00
|
|
|
}
|
2021-12-09 12:32:58 +00:00
|
|
|
// Initialize with default config
|
2022-12-03 23:36:13 +00:00
|
|
|
server.Config.Set(ServerConfig{
|
2021-12-09 12:32:58 +00:00
|
|
|
Bind: "localhost:4337",
|
|
|
|
EnableStaticServer: false,
|
2022-12-07 10:48:23 +00:00
|
|
|
KVPassword: generatePassword(),
|
2022-12-03 23:36:13 +00:00
|
|
|
})
|
2021-12-09 12:32:58 +00:00
|
|
|
// Save
|
2022-12-04 17:34:59 +00:00
|
|
|
err = db.PutJSON(ServerConfigKey, server.Config.Get())
|
2021-12-09 12:32:58 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2022-12-04 17:34:59 +00:00
|
|
|
} else {
|
|
|
|
server.Config.Set(config)
|
2021-11-21 21:36:48 +00:00
|
|
|
}
|
|
|
|
|
2022-02-01 11:35:34 +00:00
|
|
|
// Set hub
|
|
|
|
server.hub = db.Hub()
|
|
|
|
|
|
|
|
// Set password
|
|
|
|
server.hub.SetOptions(kv.HubOptions{
|
2022-12-03 23:36:13 +00:00
|
|
|
Password: server.Config.Get().KVPassword,
|
2022-02-01 11:35:34 +00:00
|
|
|
})
|
2021-11-19 18:37:42 +00:00
|
|
|
|
2024-02-25 13:46:59 +00:00
|
|
|
server.cancelConfigSub, err = db.SubscribeKey(ServerConfigKey, server.onConfigUpdate)
|
2023-05-05 13:02:23 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("error while handling subscription to HTTP config changes: %w", err)
|
|
|
|
}
|
|
|
|
|
2021-11-21 21:36:48 +00:00
|
|
|
return server, nil
|
2021-11-19 18:37:42 +00:00
|
|
|
}
|
|
|
|
|
2022-11-18 19:28:13 +00:00
|
|
|
// StatusData contains status info for the HTTP module
|
|
|
|
type StatusData struct {
|
|
|
|
Bind string
|
|
|
|
}
|
|
|
|
|
2023-05-31 12:49:45 +00:00
|
|
|
func (s *WebServer) Close() error {
|
2022-12-03 15:16:59 +00:00
|
|
|
if s.cancelConfigSub != nil {
|
|
|
|
s.cancelConfigSub()
|
|
|
|
}
|
|
|
|
|
2023-05-31 12:49:45 +00:00
|
|
|
if s.server != nil {
|
|
|
|
err := s.server.Close()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
2021-11-23 10:34:02 +00:00
|
|
|
}
|
|
|
|
|
2023-05-31 12:49:45 +00:00
|
|
|
func (s *WebServer) SetFrontend(files fs.FS) {
|
2021-11-19 18:37:42 +00:00
|
|
|
s.frontend = files
|
|
|
|
}
|
|
|
|
|
2023-05-31 12:49:45 +00:00
|
|
|
func (s *WebServer) makeMux() *http.ServeMux {
|
2021-11-19 18:37:42 +00:00
|
|
|
mux := http.NewServeMux()
|
|
|
|
|
2022-03-24 09:16:51 +00:00
|
|
|
// Register pprof
|
|
|
|
mux.HandleFunc("/debug/pprof/", pprof.Index)
|
|
|
|
mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
|
|
|
|
mux.HandleFunc("/debug/pprof/profile", pprof.Profile)
|
|
|
|
mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
|
|
|
|
mux.HandleFunc("/debug/pprof/trace", pprof.Trace)
|
2023-05-31 12:49:45 +00:00
|
|
|
mux.HandleFunc("/health", healthFunc)
|
2022-03-24 09:16:51 +00:00
|
|
|
|
2021-11-19 18:37:42 +00:00
|
|
|
if s.frontend != nil {
|
|
|
|
mux.Handle("/ui/", http.StripPrefix("/ui/", FileServerWithDefault(http.FS(s.frontend))))
|
|
|
|
}
|
|
|
|
if s.hub != nil {
|
|
|
|
mux.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) {
|
2024-03-14 12:33:52 +00:00
|
|
|
s.hub.CreateWebsocketClient(w, r, kv.ClientOptions{})
|
2021-11-19 18:37:42 +00:00
|
|
|
})
|
|
|
|
}
|
2022-12-03 23:36:13 +00:00
|
|
|
config := s.Config.Get()
|
|
|
|
if config.EnableStaticServer {
|
|
|
|
mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir(config.Path))))
|
2021-11-19 18:37:42 +00:00
|
|
|
}
|
2022-12-03 23:36:13 +00:00
|
|
|
for route, handler := range s.requestedRoutes.Copy() {
|
2022-12-03 15:16:59 +00:00
|
|
|
mux.Handle(route, handler)
|
2022-11-23 21:22:49 +00:00
|
|
|
}
|
2021-11-19 18:37:42 +00:00
|
|
|
|
|
|
|
return mux
|
|
|
|
}
|
|
|
|
|
2023-05-31 12:49:45 +00:00
|
|
|
func healthFunc(w http.ResponseWriter, _ *http.Request) {
|
|
|
|
w.WriteHeader(http.StatusOK)
|
2024-02-25 13:58:35 +00:00
|
|
|
_, _ = fmt.Fprint(w, "OK")
|
2023-05-31 12:49:45 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (s *WebServer) RegisterRoute(route string, handler http.Handler) {
|
2022-12-03 23:36:13 +00:00
|
|
|
s.requestedRoutes.SetKey(route, handler)
|
2022-12-03 15:16:59 +00:00
|
|
|
s.mux = s.makeMux()
|
|
|
|
}
|
|
|
|
|
2023-05-31 12:49:45 +00:00
|
|
|
func (s *WebServer) UnregisterRoute(route string) {
|
2022-12-03 23:36:13 +00:00
|
|
|
s.requestedRoutes.DeleteKey(route)
|
2022-12-03 15:16:59 +00:00
|
|
|
s.mux = s.makeMux()
|
2022-11-30 18:15:47 +00:00
|
|
|
}
|
|
|
|
|
2023-05-31 12:49:45 +00:00
|
|
|
func (s *WebServer) Listen() error {
|
2021-11-19 18:37:42 +00:00
|
|
|
// Start HTTP server
|
|
|
|
exit := make(chan error)
|
|
|
|
go func() {
|
|
|
|
for {
|
2023-05-31 12:49:45 +00:00
|
|
|
// Read config and make http request mux
|
2022-12-03 23:36:13 +00:00
|
|
|
config := s.Config.Get()
|
2024-03-14 12:33:52 +00:00
|
|
|
slog.Info("Starting HTTP server", slog.String("bind", config.Bind))
|
2021-11-19 18:37:42 +00:00
|
|
|
s.mux = s.makeMux()
|
2023-05-31 12:49:45 +00:00
|
|
|
|
|
|
|
// Make HTTP server instance
|
|
|
|
var err error
|
|
|
|
s.server, err = s.factory(s, config.Bind)
|
|
|
|
if err != nil {
|
|
|
|
exit <- err
|
|
|
|
return
|
2021-11-19 18:37:42 +00:00
|
|
|
}
|
2023-05-31 12:49:45 +00:00
|
|
|
|
|
|
|
// Start HTTP server
|
2024-03-14 12:33:52 +00:00
|
|
|
slog.Info("HTTP server started", slog.String("bind", config.Bind))
|
2023-05-31 12:49:45 +00:00
|
|
|
err = s.server.Start()
|
|
|
|
|
|
|
|
// If the server died, we need to see what to do
|
2024-03-14 12:33:52 +00:00
|
|
|
slog.Debug("HTTP server died", "error", err)
|
2021-11-19 18:37:42 +00:00
|
|
|
if err != nil && !errors.Is(err, http.ErrServerClosed) {
|
|
|
|
exit <- err
|
|
|
|
return
|
|
|
|
}
|
2023-05-31 12:49:45 +00:00
|
|
|
|
2021-11-19 18:37:42 +00:00
|
|
|
// Are we trying to close or restart?
|
2024-03-14 12:33:52 +00:00
|
|
|
slog.Debug("HTTP server stopped", slog.Bool("restart", s.restart.Get()))
|
2023-05-05 13:02:23 +00:00
|
|
|
if s.restart.Get() {
|
|
|
|
s.restart.Set(false)
|
2021-11-19 18:37:42 +00:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
break
|
|
|
|
}
|
2024-03-14 12:33:52 +00:00
|
|
|
slog.Debug("HTTP server stalled")
|
2021-11-19 18:37:42 +00:00
|
|
|
exit <- nil
|
|
|
|
}()
|
|
|
|
|
|
|
|
return <-exit
|
|
|
|
}
|
|
|
|
|
2023-05-31 12:49:45 +00:00
|
|
|
func (s *WebServer) onConfigUpdate(value string) {
|
2023-05-05 13:02:23 +00:00
|
|
|
oldConfig := s.Config.Get()
|
|
|
|
|
|
|
|
var config ServerConfig
|
|
|
|
err := json.Unmarshal([]byte(value), &config)
|
|
|
|
if err != nil {
|
2024-03-14 12:33:52 +00:00
|
|
|
slog.Error("Failed to unmarshal config", "error", err)
|
2023-05-05 13:02:23 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
s.Config.Set(config)
|
|
|
|
s.mux = s.makeMux()
|
|
|
|
// Restart hub if password changed
|
|
|
|
if oldConfig.KVPassword != config.KVPassword {
|
|
|
|
s.hub.SetOptions(kv.HubOptions{
|
|
|
|
Password: config.KVPassword,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
// Restart server if bind changed
|
|
|
|
if oldConfig.Bind != config.Bind {
|
|
|
|
s.restart.Set(true)
|
|
|
|
err = s.server.Shutdown(context.Background())
|
|
|
|
if err != nil {
|
2024-03-14 12:33:52 +00:00
|
|
|
slog.Error("Failed to shutdown server", "error", err)
|
2023-05-05 13:02:23 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-05-31 12:49:45 +00:00
|
|
|
func (s *WebServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
2021-11-19 18:37:42 +00:00
|
|
|
// Redirect to /ui/ if root
|
|
|
|
if r.URL.Path == "/" {
|
|
|
|
http.Redirect(w, r, "/ui/", http.StatusFound)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
s.mux.ServeHTTP(w, r)
|
|
|
|
}
|
2022-12-07 10:48:23 +00:00
|
|
|
|
|
|
|
func generatePassword() string {
|
|
|
|
b := make([]byte, 21) // To prevent padding characters, keep it a multiple of 3
|
|
|
|
_, err := crand.Read(b)
|
|
|
|
if err != nil {
|
|
|
|
// fallback to bad rand, but this will never fail
|
2024-02-25 13:46:59 +00:00
|
|
|
source := mrand.NewSource(time.Now().Unix())
|
|
|
|
return fmt.Sprintf("IS-%x%x", source.Int63(), source.Int63())
|
2022-12-07 10:48:23 +00:00
|
|
|
}
|
|
|
|
return base64.URLEncoding.EncodeToString(b)
|
|
|
|
}
|