diff --git a/internal/component/home/home.go b/internal/component/home/home.go
new file mode 100644
index 0000000..d607d57
--- /dev/null
+++ b/internal/component/home/home.go
@@ -0,0 +1,72 @@
+package home
+
+import (
+ "context"
+ "fmt"
+ "net/http"
+ "time"
+
+ "github.com/FAU-CDI/wisski-distillery/internal/component"
+ "github.com/FAU-CDI/wisski-distillery/internal/component/instances"
+ "github.com/FAU-CDI/wisski-distillery/pkg/lazy"
+ "github.com/tkw1536/goprogram/stream"
+)
+
+type Home struct {
+ component.ComponentBase
+
+ Instances *instances.Instances
+
+ RefreshInterval time.Duration
+
+ redirect lazy.Lazy[*Redirect]
+ instanceNames lazy.Lazy[map[string]struct{}]
+ homeBytes lazy.Lazy[[]byte]
+}
+
+func (*Home) Name() string { return "home" }
+
+func (*Home) Routes() []string { return []string{"/"} }
+
+func (home *Home) Handler(route string, context context.Context, io stream.IOStream) (http.Handler, error) {
+ home.updateRedirect(context, io)
+ home.updateInstances(context, io)
+ home.updateRender(context, io)
+ return home, nil
+}
+
+func (home *Home) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+ slug, ok := home.Config.SlugFromHost(r.Host)
+ switch {
+ case !ok:
+ http.NotFound(w, r)
+ case slug != "":
+ home.serveWissKI(w, slug, r)
+ default:
+ home.serveRoot(w, r)
+ }
+}
+
+func (home *Home) serveRoot(w http.ResponseWriter, r *http.Request) {
+ // not the root url => server the fallback
+ if !(r.URL.Path == "" || r.URL.Path == "/") {
+ home.redirect.Get(nil).ServeHTTP(w, r)
+ return
+ }
+
+ w.Header().Set("Content-Type", "text/html")
+ w.WriteHeader(http.StatusAccepted)
+ w.Write(home.homeBytes.Get(nil))
+}
+
+func (home *Home) serveWissKI(w http.ResponseWriter, slug string, r *http.Request) {
+ if _, ok := home.instanceNames.Get(nil)[slug]; !ok {
+ // Get(nil) guaranteed to work by precondition
+ w.WriteHeader(http.StatusNotFound)
+ fmt.Fprintf(w, "WissKI %q not found\n", slug)
+ return
+ }
+
+ w.WriteHeader(http.StatusBadGateway)
+ fmt.Fprintf(w, "WissKI %q is currently offline\n", slug)
+}
diff --git a/internal/component/home/home.html b/internal/component/home/home.html
new file mode 100644
index 0000000..3332e76
--- /dev/null
+++ b/internal/component/home/home.html
@@ -0,0 +1,25 @@
+
+
+
WissKI Distillery
+WissKI Distillery
+
+
+ For more information, see {{ .SelfRedirect }}.
+
+
+WissKIs on this Distillery
+
+ {{range .Instances}}
+ {{ if .Running }}
+
{{.Slug}}
+
+ {{.URL}}
+
+ {{ end }}
+ {{ end }}
+
+
+
+
diff --git a/internal/component/home/public.go b/internal/component/home/public.go
new file mode 100644
index 0000000..7bd3643
--- /dev/null
+++ b/internal/component/home/public.go
@@ -0,0 +1,90 @@
+package home
+
+import (
+ "bytes"
+ "context"
+ "text/template"
+ "time"
+
+ _ "embed"
+
+ "github.com/FAU-CDI/wisski-distillery/internal/component/instances"
+ "github.com/FAU-CDI/wisski-distillery/pkg/timex"
+ "github.com/tkw1536/goprogram/stream"
+ "golang.org/x/sync/errgroup"
+)
+
+func (home *Home) updateInstances(ctx context.Context, io stream.IOStream) {
+ timex.SetInterval(ctx, home.RefreshInterval, func(t time.Time) {
+ io.Printf("[%s]: reloading instance list", t.String())
+
+ names, _ := home.instanceMap()
+ home.instanceNames.Set(names)
+ })
+}
+
+func (home *Home) instanceMap() (map[string]struct{}, error) {
+ wissKIs, err := home.Instances.All()
+ if err != nil {
+ return nil, err
+ }
+
+ names := make(map[string]struct{}, len(wissKIs))
+ for _, w := range wissKIs {
+ names[w.Slug] = struct{}{}
+ }
+ return names, nil
+}
+
+func (home *Home) updateRender(ctx context.Context, io stream.IOStream) {
+ timex.SetInterval(ctx, home.RefreshInterval, func(t time.Time) {
+ io.Printf("[%s]: reloading home render", t.String())
+
+ bytes, _ := home.homeRender()
+ home.homeBytes.Set(bytes)
+ })
+}
+
+//go:embed "home.html"
+var homeHTMLStr string
+var homeTemplate = template.Must(template.New("home.html").Parse(homeHTMLStr))
+
+func (home *Home) homeRender() ([]byte, error) {
+ var context HomeContext
+
+ // setup a couple of static things
+ context.Time = time.Now().UTC()
+ context.SelfRedirect = home.Config.SelfRedirect.String()
+
+ // find all the WissKIs
+ wissKIs, err := home.Instances.All()
+ if err != nil {
+ return nil, err
+ }
+ context.Instances = make([]instances.WissKIInfo, len(wissKIs))
+
+ // determine their infos
+ var eg errgroup.Group
+ for i, instance := range wissKIs {
+ i := i
+ wissKI := instance
+ eg.Go(func() (err error) {
+ context.Instances[i], err = wissKI.Info(true)
+ return
+ })
+ }
+ eg.Wait()
+
+ // render the template
+ var buffer bytes.Buffer
+ homeTemplate.Execute(&buffer, context)
+ return buffer.Bytes(), nil
+}
+
+type HomeContext struct {
+ Instances []instances.WissKIInfo
+
+ Time time.Time
+
+ SelfRedirect string
+}
diff --git a/internal/component/control/extras_self.go b/internal/component/home/redirect.go
similarity index 61%
rename from internal/component/control/extras_self.go
rename to internal/component/home/redirect.go
index c9bb4fb..9c652b2 100644
--- a/internal/component/control/extras_self.go
+++ b/internal/component/home/redirect.go
@@ -1,36 +1,35 @@
-package control
+package home
import (
"context"
"encoding/json"
- "fmt"
"net/http"
"strings"
+ "time"
- "github.com/FAU-CDI/wisski-distillery/internal/component"
- "github.com/FAU-CDI/wisski-distillery/internal/component/instances"
+ "github.com/FAU-CDI/wisski-distillery/pkg/timex"
"github.com/tkw1536/goprogram/stream"
)
-// SelfHandler implements serving the '/' route
-type SelfHandler struct {
- component.ComponentBase
-
- Instances *instances.Instances
+func (home *Home) updateRedirect(ctx context.Context, io stream.IOStream) {
+ timex.SetInterval(ctx, home.RefreshInterval, func(t time.Time) {
+ io.Printf("[%s]: reloading overrides", t.String())
+ redirect, _ := home.loadRedirect()
+ home.redirect.Set(&redirect)
+ })
}
-func (SelfHandler) Name() string { return "control-self" }
+func (home *Home) loadRedirect() (redirect Redirect, err error) {
+ if redirect.Overrides == nil {
+ redirect.Overrides = make(map[string]string)
+ }
+ redirect.Overrides[""] = home.Config.SelfRedirect.String()
-func (*SelfHandler) Routes() []string { return []string{"/"} }
+ redirect.Absolute = false
+ redirect.Permanent = false
-func (sh *SelfHandler) Handler(route string, context context.Context, io stream.IOStream) (http.Handler, error) {
- // create a redirect
- var redirect Redirect
- var err error
-
- // open the overrides file
- overrides, err := sh.Environment.Open(sh.Config.SelfOverridesFile)
- io.Printf("loading overrides from %q\n", sh.Config.SelfOverridesFile)
+ // load the overrides file
+ overrides, err := home.Environment.Open(home.Config.SelfOverridesFile)
if err != nil {
return redirect, err
}
@@ -38,52 +37,13 @@ func (sh *SelfHandler) Handler(route string, context context.Context, io stream.
// decode the overrides file
if err := json.NewDecoder(overrides).Decode(&redirect.Overrides); err != nil {
- return nil, err
+ return redirect, err
}
- if redirect.Overrides == nil {
- redirect.Overrides = make(map[string]string)
- }
- redirect.Overrides[""] = sh.Config.SelfRedirect.String()
-
- // create a redirect server
- redirect.Fallback, err = sh.selfFallback()
- if err != nil {
- return nil, err
- }
- redirect.Absolute = false
- redirect.Permanent = false
-
// and return!
return redirect, nil
}
-func (sh *SelfHandler) selfFallback() (http.Handler, error) {
- return http.HandlerFunc(sh.serveFallback), nil
-}
-
-var notFoundText = []byte("not found")
-
-func (sh *SelfHandler) serveFallback(w http.ResponseWriter, r *http.Request) {
-
- slug := sh.Config.SlugFromHost(r.Host)
- if slug == "" {
- w.WriteHeader(http.StatusNotFound)
- w.Write(notFoundText)
- return
- }
-
- if ok, _ := sh.Instances.Has(slug); !ok {
- w.WriteHeader(http.StatusNotFound)
- fmt.Fprintf(w, "WissKI %q not found\n", slug)
- return
- }
-
- w.WriteHeader(http.StatusBadGateway)
- fmt.Fprintf(w, "WissKI %q is currently offline\n", slug)
-
-}
-
// Redirect implements a redirect server that redirects all requests.
// It implements http.Handler.
type Redirect struct {
diff --git a/internal/component/resolver/prefixes.go b/internal/component/resolver/prefixes.go
new file mode 100644
index 0000000..689ea53
--- /dev/null
+++ b/internal/component/resolver/prefixes.go
@@ -0,0 +1,50 @@
+package resolver
+
+import (
+ "context"
+ "time"
+
+ "github.com/FAU-CDI/wisski-distillery/pkg/timex"
+ "github.com/tkw1536/goprogram/stream"
+)
+
+// updatePrefixes starts updating prefixes
+func (resolver *Resolver) updatePrefixes(io stream.IOStream, ctx context.Context) {
+ timex.SetInterval(ctx, resolver.RefreshInterval, func(t time.Time) {
+ io.Printf("[%s]: reloading prefixes", t.String())
+ prefixes, _ := resolver.AllPrefixes()
+ resolver.prefixes.Set(prefixes)
+ })
+}
+
+// AllPrefixes returns a list of all prefixes from the server.
+// Prefixes may be cached on the server
+func (resolver *Resolver) AllPrefixes() (map[string]string, error) {
+ instances, err := resolver.Instances.All()
+ if err != nil {
+ return nil, err
+ }
+
+ gPrefixes := make(map[string]string)
+ var lastErr error
+ for _, instance := range instances {
+ if instance.NoPrefix() {
+ continue
+ }
+ url := instance.URL().String()
+
+ // failed to fetch prefixes for this particular instance
+ // => skip it!
+ prefixes, err := instance.PrefixesCached()
+ if err != nil {
+ lastErr = err
+ continue
+ }
+
+ for _, p := range prefixes {
+ gPrefixes[p] = url
+ }
+ }
+
+ return gPrefixes, lastErr
+}
diff --git a/internal/component/resolver/resolver.go b/internal/component/resolver/resolver.go
index df4c56c..bedef61 100644
--- a/internal/component/resolver/resolver.go
+++ b/internal/component/resolver/resolver.go
@@ -52,7 +52,8 @@ func (resolver *Resolver) Handler(route string, context context.Context, io stre
io.Printf("registering legacy domain %s\n", domain)
}
- go resolver.updatePrefixes(io, context)
+ // start updating prefixes
+ resolver.updatePrefixes(io, context)
// resolve the prefixes
p.Resolver = resolvers.InOrder{
@@ -63,62 +64,11 @@ func (resolver *Resolver) Handler(route string, context context.Context, io stre
}), err
}
-func (resolver *Resolver) updatePrefixes(io stream.IOStream, ctx context.Context) {
- t := time.NewTicker(resolver.RefreshInterval)
- defer t.Stop()
-
- for {
- select {
- case <-t.C:
- io.Println("resolver: Reloading prefixes from database")
- prefixes, _ := resolver.AllPrefixes()
- resolver.prefixes.Set(prefixes)
- case <-ctx.Done():
- return
- }
- }
-}
-
func (resolver *Resolver) Target(uri string) string {
return wdresolve.PrefixTarget(resolver, uri)
}
// Prefixes returns a cached list of prefixes
func (resolver *Resolver) Prefixes() (prefixes map[string]string) {
- return resolver.prefixes.Get(func() map[string]string {
- prefixes, _ := resolver.AllPrefixes()
- return prefixes
- })
-}
-
-// AllPrefixes returns a list of all prefixes from the server.
-// Prefixes may be cached on the server
-func (resolver *Resolver) AllPrefixes() (map[string]string, error) {
- instances, err := resolver.Instances.All()
- if err != nil {
- return nil, err
- }
-
- gPrefixes := make(map[string]string)
- var lastErr error
- for _, instance := range instances {
- if instance.NoPrefix() {
- continue
- }
- url := instance.URL().String()
-
- // failed to fetch prefixes for this particular instance
- // => skip it!
- prefixes, err := instance.PrefixesCached()
- if err != nil {
- lastErr = err
- continue
- }
-
- for _, p := range prefixes {
- gPrefixes[p] = url
- }
- }
-
- return gPrefixes, lastErr
+ return resolver.prefixes.Get(nil) // by precondition there always is a cached value
}
diff --git a/internal/config/domains.go b/internal/config/domains.go
index a465d25..f7c637a 100644
--- a/internal/config/domains.go
+++ b/internal/config/domains.go
@@ -42,18 +42,26 @@ func (cfg Config) DefaultSSLHost() string {
return cfg.IfHttps(cfg.DefaultHost())
}
-// SlugFromHost returns the slug belonging to the appropriate host.
-func (cfg Config) SlugFromHost(host string) (slug string) {
+// SlugFromHost returns the slug belonging to the appropriate host.'
+//
+// When host is a top-level domain, returns "", true.
+// When no slug is found, returns "", false.
+func (cfg Config) SlugFromHost(host string) (slug string, ok bool) {
// extract an ':port' that happens to be in the host.
domain, _, _ := strings.Cut(host, ":")
+ domainL := strings.ToLower(domain)
// check all the possible domain endings
for _, suffix := range append([]string{cfg.DefaultDomain}, cfg.SelfExtraDomains...) {
- if strings.HasSuffix(domain, "."+suffix) {
- return domain[:len(domain)-len(suffix)-1]
+ suffixL := strings.ToLower(suffix)
+ if domainL == suffixL {
+ return "", true
+ }
+ if strings.HasSuffix(domainL, "."+suffixL) {
+ return domain[:len(domain)-len(suffix)-1], true
}
}
// no domain found!
- return ""
+ return "", ok
}
diff --git a/internal/dis/component.go b/internal/dis/component.go
index 045711f..094ce71 100644
--- a/internal/dis/component.go
+++ b/internal/dis/component.go
@@ -5,6 +5,7 @@ import (
"github.com/FAU-CDI/wisski-distillery/internal/component"
"github.com/FAU-CDI/wisski-distillery/internal/component/control"
+ "github.com/FAU-CDI/wisski-distillery/internal/component/home"
"github.com/FAU-CDI/wisski-distillery/internal/component/instances"
"github.com/FAU-CDI/wisski-distillery/internal/component/resolver"
"github.com/FAU-CDI/wisski-distillery/internal/component/snapshots"
@@ -43,7 +44,9 @@ func (dis *Distillery) register(context *component.PoolContext) []component.Comp
// Control server
ra[*control.Control](dis, context),
- ra[*control.SelfHandler](dis, context),
+ r(dis, context, func(home *home.Home) {
+ home.RefreshInterval = time.Minute
+ }),
r(dis, context, func(resolver *resolver.Resolver) {
resolver.RefreshInterval = time.Minute
}),
diff --git a/pkg/timex/timex.go b/pkg/timex/timex.go
new file mode 100644
index 0000000..c8562c8
--- /dev/null
+++ b/pkg/timex/timex.go
@@ -0,0 +1,25 @@
+package timex
+
+import (
+ "context"
+ "time"
+)
+
+// SetInterval invokes f with the current time and then spawns a new goroutine that runs f every d, until context is closed.
+func SetInterval(ctx context.Context, d time.Duration, f func(t time.Time)) {
+ f(time.Now())
+
+ go func() {
+ t := time.NewTicker(d)
+ defer t.Stop()
+
+ for {
+ select {
+ case tick := <-t.C:
+ f(tick)
+ case <-ctx.Done():
+ return
+ }
+ }
+ }()
+}