diff --git a/doc/configuration.md b/doc/configuration.md index fd74a56..52640a3 100644 --- a/doc/configuration.md +++ b/doc/configuration.md @@ -19,7 +19,10 @@ Voici un exemple commenté du fichier de configuration: "http": { // Couple
: d'écoute // Par défaut ":" i.e. toutes les adresses avec port aléatoire - "address": ":" + "address": ":", + // Répertoire de personnalisation de la page d'accueil + // Voir section "Personnalisation" ci-dessous + "customDir": "${CONFIG_DIR}/custom" }, // Configuration du serveur HTTPS "https": { @@ -48,3 +51,9 @@ Voici un exemple commenté du fichier de configuration: } } ``` + +## Personnalisation + +Il est possible de personnaliser la page d'accueil du player Arcast en créant des fichiers dans le répertoire définit par l'attribut de configuration `http.customDir`. + +Le contenu de ce répertoire doit répliquer l'arborescence embarquée par défaut (voir https://forge.cadoles.com/arcad/arcast/src/branch/develop/pkg/server/embed). Chaque fichier présent remplacera celui embarqué par défaut. diff --git a/go.mod b/go.mod index 4db6730..cbc2c70 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.21.4 require ( gioui.org v0.4.1 + github.com/davecgh/go-spew v1.1.1 github.com/gioui-plugins/gio-plugins v0.0.0-20240323070753-3331d8c2df5d github.com/go-chi/cors v1.2.1 github.com/gorilla/websocket v1.5.1 @@ -23,6 +24,7 @@ require ( github.com/cenkalti/backoff v2.2.1+incompatible // indirect github.com/charmbracelet/lipgloss v0.7.1 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect + github.com/dschmidt/go-layerfs v0.1.0 // indirect github.com/go-playground/locales v0.14.0 // indirect github.com/go-playground/universal-translator v0.18.0 // indirect github.com/go-text/typesetting v0.0.0-20230803102845-24e03d8b5372 // indirect diff --git a/go.sum b/go.sum index 3b0db09..6d627d3 100644 --- a/go.sum +++ b/go.sum @@ -32,6 +32,8 @@ github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46t github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dschmidt/go-layerfs v0.1.0 h1:jE6aHDfjNzS/31DS48th6EkmELwTa1Uf+aO4jRkBs3U= +github.com/dschmidt/go-layerfs v0.1.0/go.mod h1:m62aff0hn23Q/tQBRiNSeLD7EUuimDvsuCvCpzBr3Gw= github.com/gioui-plugins/gio-plugins v0.0.0-20240323070753-3331d8c2df5d h1:8b7owUJ8sNmgqEk+1d7ylr3TCH3vliCvY/6ycfize8o= github.com/gioui-plugins/gio-plugins v0.0.0-20240323070753-3331d8c2df5d/go.mod h1:3XVleuCdPpdajFL+ASh2wmXZNskitXQQ4jhVss0VHZg= github.com/go-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk= diff --git a/internal/command/player/run.go b/internal/command/player/run.go index 8601007..0106521 100644 --- a/internal/command/player/run.go +++ b/internal/command/player/run.go @@ -68,6 +68,11 @@ func Run() *cli.Command { EnvVars: []string{"ARCAST_DESKTOP_ALLOWED_ORIGINS"}, Value: cli.NewStringSlice(), }, + &cli.StringFlag{ + Name: "custom-files-dir", + EnvVars: []string{"ARCAST_DESKTOP_CUSTOM_FILES_DIR"}, + Value: "", + }, &cli.BoolFlag{ Name: "dummy-browser", EnvVars: []string{"ARCAST_DESKTOP_DUMMY_BROWSER"}, @@ -114,21 +119,11 @@ func Run() *cli.Command { conf := config.DefaultConfig() - logger.Info(ctx.Context, "loading or creating configuration file", logger.F("filename", configFile)) - if err := config.LoadOrCreate(ctx.Context, configFile, conf, config.DefaultTransforms...); err != nil { - logger.Error(ctx.Context, "could not load configuration file", logger.CapturedE(errors.WithStack(err))) - } - instanceID := ctx.String("instance-id") if instanceID != "" { conf.InstanceID = instanceID } - cert, err := tls.X509KeyPair(conf.HTTPS.Cert, conf.HTTPS.Key) - if err != nil { - return errors.Wrap(err, "could not parse tls cert/key pair") - } - if ctx.IsSet("apps") { conf.Apps.Enabled = ctx.Bool("apps") } @@ -145,6 +140,20 @@ func Run() *cli.Command { conf.AllowedOrigins = ctx.StringSlice("allowed-origins") } + if ctx.IsSet("custom-dir") { + conf.HTTP.CustomDir = ctx.String("custom-dir") + } + + logger.Info(ctx.Context, "loading or creating configuration file", logger.F("filename", configFile)) + if err := config.LoadOrCreate(ctx.Context, configFile, conf, config.DefaultTransforms...); err != nil { + logger.Error(ctx.Context, "could not load configuration file", logger.CapturedE(errors.WithStack(err))) + } + + cert, err := tls.X509KeyPair(conf.HTTPS.Cert, conf.HTTPS.Key) + if err != nil { + return errors.Wrap(err, "could not parse tls cert/key pair") + } + server := server.New(browser, server.WithInstanceID(conf.InstanceID), server.WithAppsEnabled(conf.Apps.Enabled), @@ -154,6 +163,7 @@ func Run() *cli.Command { server.WithTLSAddress(conf.HTTPS.Address), server.WithTLSCertificate(&cert), server.WithAllowedOrigins(conf.AllowedOrigins...), + server.WithUpperLayerDir(conf.HTTP.CustomDir), ) if err := server.Start(); err != nil { diff --git a/modd.conf b/modd.conf index ee0595c..31b201b 100644 --- a/modd.conf +++ b/modd.conf @@ -3,7 +3,7 @@ } **/*.go -pkg/server/templates/**.gotmpl +pkg/server/embed/** modd.conf .env { prep: make build-client diff --git a/pkg/config/config.go b/pkg/config/config.go index db89080..c4cf427 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -20,7 +20,8 @@ type Config struct { } type HTTPConfig struct { - Address string `json:"address"` + Address string `json:"address"` + CustomDir string `json:"customDir"` } type HTTPSConfig struct { @@ -40,7 +41,7 @@ type AppsConfig struct { DefaultApp string `json:"defaultApp"` } -type TransformFunc func(ctx context.Context, conf *Config) error +type TransformFunc func(ctx context.Context, filename string, conf *Config) error func DefaultConfigFile(ctx context.Context) string { configDir, err := os.UserConfigDir() @@ -69,7 +70,7 @@ func LoadOrCreate(ctx context.Context, filename string, conf *Config, funcs ...T } for _, fn := range funcs { - if err := fn(ctx, conf); err != nil { + if err := fn(ctx, filename, conf); err != nil { return errors.WithStack(err) } } @@ -99,7 +100,8 @@ func DefaultConfig() *Config { InstanceID: server.NewRandomInstanceID(), AllowedOrigins: []string{}, HTTP: HTTPConfig{ - Address: ":45555", + Address: ":45555", + CustomDir: "", }, HTTPS: HTTPSConfig{ Address: ":45556", diff --git a/pkg/config/custom_files.go b/pkg/config/custom_files.go new file mode 100644 index 0000000..00e48fc --- /dev/null +++ b/pkg/config/custom_files.go @@ -0,0 +1,28 @@ +package config + +import ( + "context" + "os" + "path/filepath" + + "github.com/pkg/errors" + "gitlab.com/wpetit/goweb/logger" +) + +func CreateCustomDir(ctx context.Context, filename string, conf *Config) error { + if conf.HTTP.CustomDir != "" { + return nil + } + + configDir := filepath.Dir(filename) + customFilesDir := filepath.Join(configDir, "custom") + + if err := os.MkdirAll(customFilesDir, 0755); err != nil { + logger.Error(ctx, "could not create custom files directory", logger.CapturedE(errors.WithStack(err))) + return nil + } + + conf.HTTP.CustomDir = customFilesDir + + return nil +} diff --git a/pkg/config/default_transforms.go b/pkg/config/default_transforms.go index f36a0e6..5a33a0d 100644 --- a/pkg/config/default_transforms.go +++ b/pkg/config/default_transforms.go @@ -3,4 +3,5 @@ package config var DefaultTransforms = []TransformFunc{ GenerateSelfSignedCert, RenewExpiredSelfSignedCert, + CreateCustomDir, } diff --git a/pkg/config/selfsigned.go b/pkg/config/selfsigned.go index 42ffa98..5ba88ee 100644 --- a/pkg/config/selfsigned.go +++ b/pkg/config/selfsigned.go @@ -15,7 +15,7 @@ import ( "gitlab.com/wpetit/goweb/logger" ) -func GenerateSelfSignedCert(ctx context.Context, conf *Config) error { +func GenerateSelfSignedCert(ctx context.Context, filename string, conf *Config) error { if !conf.HTTPS.SelfSigned.Enabled { return nil } @@ -42,13 +42,13 @@ func GenerateSelfSignedCert(ctx context.Context, conf *Config) error { return nil } -func RenewExpiredSelfSignedCert(ctx context.Context, conf *Config) error { +func RenewExpiredSelfSignedCert(ctx context.Context, filename string, conf *Config) error { if !conf.HTTPS.SelfSigned.Enabled { return nil } if conf.HTTPS.Cert == nil || conf.HTTPS.Key == nil { - if err := GenerateSelfSignedCert(ctx, conf); err != nil { + if err := GenerateSelfSignedCert(ctx, filename, conf); err != nil { return errors.WithStack(err) } } @@ -62,7 +62,7 @@ func RenewExpiredSelfSignedCert(ctx context.Context, conf *Config) error { if err != nil { logger.Error(ctx, "could not parse x509 certificate, regenerating one", logger.CapturedE(errors.WithStack(err))) - if err := GenerateSelfSignedCert(ctx, conf); err != nil { + if err := GenerateSelfSignedCert(ctx, filename, conf); err != nil { return errors.WithStack(err) } } @@ -74,7 +74,7 @@ func RenewExpiredSelfSignedCert(ctx context.Context, conf *Config) error { logger.Warn(ctx, "self-signed certificate has expired, regenerating one", logger.CapturedE(errors.WithStack(err))) - if err := GenerateSelfSignedCert(ctx, conf); err != nil { + if err := GenerateSelfSignedCert(ctx, filename, conf); err != nil { return errors.WithStack(err) } diff --git a/pkg/server/embed/_partials/base.gohtml b/pkg/server/embed/_partials/base.gohtml new file mode 100644 index 0000000..25d01d4 --- /dev/null +++ b/pkg/server/embed/_partials/base.gohtml @@ -0,0 +1,64 @@ +{{ define "base" }} + + + + + + + Ready to cast ! + + {{ block "head" . + }}{{ + end + }} + + +
+
+
+ {{ block "message" . }} +

Ready to cast !

+ {{ end }} + {{ block "info" . }} +

Instance ID

+

+ {{ .ID }} +

+

Addresses

+
    + {{ $port := .Port }} + {{ + range.IPs + }} +
  • + {{ . }}:{{ $port }} +
  • + {{ + end + }} +
+ {{ end }} + {{if .Apps }} + {{ block "apps" . }} +

Apps

+ + {{ end }} + {{ end }} +
+
+ + +{{ end }} diff --git a/pkg/server/embed/_templates/index.gohtml b/pkg/server/embed/_templates/index.gohtml new file mode 100644 index 0000000..96c49ba --- /dev/null +++ b/pkg/server/embed/_templates/index.gohtml @@ -0,0 +1,3 @@ +{{ define "index" }} +{{ template "base" . }} +{{ end }} diff --git a/pkg/server/embed/logo.png b/pkg/server/embed/logo.png new file mode 100644 index 0000000..564b6a7 Binary files /dev/null and b/pkg/server/embed/logo.png differ diff --git a/pkg/server/embed/style.css b/pkg/server/embed/style.css new file mode 100644 index 0000000..3e7cdeb --- /dev/null +++ b/pkg/server/embed/style.css @@ -0,0 +1,121 @@ +html { + box-sizing: border-box; + font-size: 16px; + width: 100%; + height: 100%; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", + "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", + sans-serif; + width: 100%; + height: 100%; + background: rgb(76, 96, 188); + background: linear-gradient( + 415deg, + rgba(4, 168, 243, 1), + rgb(76, 136, 188, 1), + rgba(76, 96, 188, 1), + rgb(115, 76, 188, 1), + rgb(87, 76, 188, 1) + ); + background-size: 400% 400%; + animation: gradient 15s ease infinite; +} + +@keyframes gradient { + 0% { + background-position: 0% 50%; + } + 50% { + background-position: 100% 50%; + } + 100% { + background-position: 0% 50%; + } +} + +*, +*:before, +*:after { + box-sizing: inherit; +} + +ol, +ul { + list-style: none; +} + +body, +h1, +h2, +h3, +h4, +h5, +h6, +p, +ol, +ul { + margin: 0; + padding: 0; + font-weight: normal; +} + +.container { + width: 100%; + height: 100%; + display: flex; + justify-content: center; + align-items: center; +} + +.panel { + display: block; + background-color: #fff; + border-radius: 15px; + box-shadow: 10px 10px 10px #33333361; + position: relative; + padding: 50px 30px 30px 30px; + min-width: 50%; + color: #333; +} + +#title { + margin: 10px 0px 20px 0px; +} + +#icon { + width: 100px; + aspect-ratio: 1/1; + background-size: contain; + background-position: center center; + background-image: url("logo.png"); + position: absolute; + left: 50%; + margin-left: -50px; + margin-top: -100px; + background-repeat: no-repeat; +} + +.panel p, +.panel ul { + margin-top: 10px; +} + +.text-centered { + text-align: center; +} + +.text-italic { + font-style: italic; +} + +.text-small { + font-size: 0.8em; +} + +.mt { + margin-top: 1em; + display: block; +} diff --git a/pkg/server/fs.go b/pkg/server/fs.go new file mode 100644 index 0000000..a2613bf --- /dev/null +++ b/pkg/server/fs.go @@ -0,0 +1,90 @@ +package server + +import ( + "embed" + "html/template" + "io/fs" + "net/http" + "os" + "strings" + + "forge.cadoles.com/arcad/arcast/pkg/network" + "github.com/dschmidt/go-layerfs" + "github.com/pkg/errors" + "gitlab.com/wpetit/goweb/logger" + + _ "embed" +) + +var ( + //go:embed embed/** + embedFS embed.FS +) + +func (s *Server) initLayeredFS() error { + layers := make([]fs.FS, 0) + + if s.upperLayerDir != "" { + upperLayer := os.DirFS(s.upperLayerDir) + layers = append(layers, upperLayer) + } + + baseLayer, err := fs.Sub(embedFS, "embed") + if err != nil { + + return errors.WithStack(err) + } + + layers = append(layers, baseLayer) + + s.layeredFS = layerfs.New(layers...) + + return nil +} + +func (s *Server) handleStatic(w http.ResponseWriter, r *http.Request) { + path := r.URL.Path + + if strings.HasPrefix(path, "/_templates") { + http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) + return + } + + http.ServeFileFS(w, r, s.layeredFS, path) +} + +func (s *Server) handleIndex(w http.ResponseWriter, r *http.Request) { + type templateData struct { + IPs []string + Port int + TLSPort int + ID string + Apps bool + } + + ips, err := network.GetLANIPv4Addrs() + if err != nil { + logger.Error(r.Context(), "could not retrieve lan ip addresses", logger.CapturedE(errors.WithStack(err))) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } + + d := templateData{ + ID: s.instanceID, + IPs: ips, + Port: s.port, + TLSPort: s.tlsPort, + Apps: s.appsEnabled, + } + + templates, err := template.New("").ParseFS(s.layeredFS, "_partials/*.gohtml", "_templates/*.gohtml") + if err != nil { + logger.Error(r.Context(), "could not parse template", logger.CapturedE(errors.WithStack(err))) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } + + if err := templates.ExecuteTemplate(w, "index", d); err != nil { + logger.Error(r.Context(), "could not render index page", logger.CapturedE(errors.WithStack(err))) + } +} diff --git a/pkg/server/http.go b/pkg/server/http.go index db45a98..254f98e 100644 --- a/pkg/server/http.go +++ b/pkg/server/http.go @@ -4,7 +4,6 @@ import ( "context" "crypto/tls" "fmt" - "html/template" "net" "net/http" "strconv" @@ -14,25 +13,8 @@ import ( "github.com/go-chi/cors" "github.com/pkg/errors" "gitlab.com/wpetit/goweb/logger" - - _ "embed" ) -var ( - //go:embed templates/idle.html.gotmpl - rawIdleTemplate []byte - idleTemplate *template.Template -) - -func init() { - tmpl, err := template.New("").Parse(string(rawIdleTemplate)) - if err != nil { - panic(errors.Wrap(err, "could not parse idle template")) - } - - idleTemplate = tmpl -} - func (s *Server) startWebServers(ctx context.Context) error { router := chi.NewRouter() @@ -50,7 +32,6 @@ func (s *Server) startWebServers(ctx context.Context) error { })) } - router.Get("/", s.handleHome) router.Get("/api/v1/info", s.handleInfo) router.Post("/api/v1/cast", s.handleCast) router.Delete("/api/v1/cast", s.handleReset) @@ -63,6 +44,9 @@ func (s *Server) startWebServers(ctx context.Context) error { router.Handle("/api/v1/broadcast/{channelID}", http.HandlerFunc(s.handleBroadcast)) } + router.Get("/", s.handleIndex) + router.Get("/*", s.handleStatic) + if err := s.startHTTPServer(ctx, router); err != nil { return errors.WithStack(err) } @@ -193,32 +177,3 @@ func (s *Server) getAllowedOrigins() ([]string, error) { return allowedOrigins, nil } - -func (s *Server) handleHome(w http.ResponseWriter, r *http.Request) { - type templateData struct { - IPs []string - Port int - TLSPort int - ID string - Apps bool - } - - ips, err := network.GetLANIPv4Addrs() - if err != nil { - logger.Error(r.Context(), "could not retrieve lan ip addresses", logger.CapturedE(errors.WithStack(err))) - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - return - } - - d := templateData{ - ID: s.instanceID, - IPs: ips, - Port: s.port, - TLSPort: s.tlsPort, - Apps: s.appsEnabled, - } - - if err := idleTemplate.Execute(w, d); err != nil { - logger.Error(r.Context(), "could not render idle page", logger.CapturedE(errors.WithStack(err))) - } -} diff --git a/pkg/server/options.go b/pkg/server/options.go index 8970c1a..34acb4d 100644 --- a/pkg/server/options.go +++ b/pkg/server/options.go @@ -28,6 +28,7 @@ type Options struct { DefaultApp string AllowedOrigins []string Apps []App + UpperLayerDir string } type OptionFunc func(opts *Options) @@ -42,6 +43,7 @@ func NewOptions(funcs ...OptionFunc) *Options { DefaultApp: "", AllowedOrigins: make([]string, 0), Apps: make([]App, 0), + UpperLayerDir: "", } for _, fn := range funcs { @@ -105,6 +107,12 @@ func WithServiceDiscoveryEnabled(enabled bool) OptionFunc { } } +func WithUpperLayerDir(dir string) OptionFunc { + return func(opts *Options) { + opts.UpperLayerDir = dir + } +} + func NewRandomInstanceID() string { return newRandomInstanceID() } diff --git a/pkg/server/server.go b/pkg/server/server.go index 9247131..704b16e 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -3,6 +3,7 @@ package server import ( "context" "crypto/tls" + "io/fs" "forge.cadoles.com/arcad/arcast/pkg/browser" "github.com/gorilla/websocket" @@ -32,10 +33,19 @@ type Server struct { ctx context.Context cancel context.CancelFunc upgrader websocket.Upgrader + + layeredFS fs.FS + upperLayerDir string } func (s *Server) Start() error { - serverCtx, cancelServer := context.WithCancel(context.Background()) + ctx := context.Background() + + if err := s.initLayeredFS(); err != nil { + return errors.WithStack(err) + } + + serverCtx, cancelServer := context.WithCancel(ctx) s.cancel = cancelServer s.ctx = serverCtx @@ -92,6 +102,7 @@ func New(browser browser.Browser, funcs ...OptionFunc) *Server { defaultApp: opts.DefaultApp, apps: opts.Apps, serviceDiscoveryEnabled: opts.EnableServiceDiscovery, + upperLayerDir: opts.UpperLayerDir, } server.upgrader = websocket.Upgrader{ diff --git a/pkg/server/templates/idle.html.gotmpl b/pkg/server/templates/idle.html.gotmpl deleted file mode 100644 index f7cc94f..0000000 --- a/pkg/server/templates/idle.html.gotmpl +++ /dev/null @@ -1,173 +0,0 @@ - - - - - - Arcast - Ready to cast ! - - - - -
-
-
-

Ready to cast !

-

Instance ID

-

- {{ .ID }} -

-

Addresses

-
    - {{ $port := .Port }} - {{ - range.IPs - }} -
  • - {{ . }}:{{ $port }} -
  • - {{ - end - }} -
- {{if .Apps }} -

Apps

- - {{ end }} -
-
- -