diff --git a/internal/config/config.yml b/internal/config/config.yml index 22964ed..0980cd2 100644 --- a/internal/config/config.yml +++ b/internal/config/config.yml @@ -39,6 +39,10 @@ http: # This email address can be configured here. certbot_email: null + # Serve the panel also on the toplevel domain, and not only on the "_panel" domain. + # Enabled by default. + panel: null + # Enable or Disable the HTTP API. # In the future, it will be enabled by default, but at this point it is not. api: null diff --git a/internal/config/http.go b/internal/config/http.go index c09dfaa..5b0e560 100644 --- a/internal/config/http.go +++ b/internal/config/http.go @@ -25,6 +25,11 @@ type HTTPConfig struct { // This email address can be configured here. CertbotEmail string `yaml:"certbot_email" validate:"email"` + // Also serve the panel on the toplevel domain. + // Note that the panel is *always* servered under the "_panel" domain. + // Disabling this is not recommended. + Panel validators.NullableBool `yaml:"panel" validate:"bool" default:"true"` + // API determines if the API is enabled. // In a future version of the distillery, it will be enabled by default. API validators.NullableBool `yaml:"api" validate:"bool" default:"false"` @@ -36,6 +41,16 @@ type HTTPConfig struct { PhpMyAdmin validators.NullableBool `yaml:"phpmyadmin" validate:"bool" default:"false"` } +func (hcfg HTTPConfig) PublicTopDomain() string { + // if we have panel domain enabled, then return it + if hcfg.Panel.Set && hcfg.Panel.Value { + return hcfg.PrimaryDomain + } + + // else use the domain itself + return hcfg.Domains(PanelDomain.Domain())[0] +} + // TSDomain returns the full url to the triplestore, if any func (hcfg HTTPConfig) TSURL() template.URL { return hcfg.optionalURL(TriplestoreDomain.Domain(), hcfg.TS) @@ -66,7 +81,7 @@ func (hcfg HTTPConfig) optionalURL(domain string, enabled validators.NullableBoo func (hcfg HTTPConfig) JoinPath(elem ...string) *url.URL { u := url.URL{ Scheme: "http", - Host: hcfg.PrimaryDomain, + Host: hcfg.PublicTopDomain(), Path: "/", } if hcfg.HTTPSEnabled() { @@ -93,6 +108,7 @@ func (hcfg HTTPConfig) HTTPSEnabled() bool { type SpecialDomain string var ( + PanelDomain SpecialDomain = "panel" TriplestoreDomain SpecialDomain = "ts" PHPMyAdminDomain SpecialDomain = "phpmyadmin" ) @@ -168,6 +184,27 @@ func (cfg HTTPConfig) SlugFromHost(host string) (slug string, ok bool) { return "", ok } +// NormSlugFromHost is like SlugFromHost, but normalizes the panel host +func (cfg HTTPConfig) NormSlugFromHost(host string) (string, bool) { + // if we didn't get a domain, don't do anything + slug, ok := cfg.SlugFromHost(host) + if !ok { + return "", false + } + + // always serve the panel domain + if slug == PanelDomain.Domain() { + return "", true + } + + // if we don't serve the toplevel domain then the toplevel domain is an error. + if slug == "" && !(cfg.Panel.Set && cfg.Panel.Value) { + return "", false + } + + return slug, true +} + func TrimSuffixFold(s string, suffix string) string { if len(s) >= len(suffix) && strings.EqualFold(s[len(s)-len(suffix):], suffix) { return s[:len(s)-len(suffix)] @@ -175,10 +212,13 @@ func TrimSuffixFold(s string, suffix string) string { return s } -// DefaultHostRule returns the default traefik hostname rule for this distillery. -// This consists of the [DefaultDomain] as well as [ExtraDomains]. -func (cfg HTTPConfig) DefaultHostRule() string { - return cfg.HostRule("") +// DefaultHostRule returns the host rule for the control panel of this distillery. +func (cfg HTTPConfig) PanelHostRule() string { + all := cfg.Domains(PanelDomain.Domain()) + if cfg.Panel.Set && cfg.Panel.Value { + all = append(all, cfg.Domains("")...) + } + return MakeHostRule(all...) } // MakeHostRule builds a new Host() rule string to be used by traefik. diff --git a/internal/dis/component/auth/panel/ssh.go b/internal/dis/component/auth/panel/ssh.go index 225b18d..ad7f8ca 100644 --- a/internal/dis/component/auth/panel/ssh.go +++ b/internal/dis/component/auth/panel/ssh.go @@ -62,7 +62,7 @@ func (panel *UserPanel) sshRoute(ctx context.Context) http.Handler { return sc, err } - sc.Domain = panel.Config.HTTP.PrimaryDomain + sc.Domain = panel.Config.HTTP.PublicTopDomain() sc.Port = panel.Config.Listen.SSHPort // pick the first domain that the user has access to as an example diff --git a/internal/dis/component/server/home/home.go b/internal/dis/component/server/home/home.go index 5fbbb9f..e168843 100644 --- a/internal/dis/component/server/home/home.go +++ b/internal/dis/component/server/home/home.go @@ -42,7 +42,7 @@ func (home *Home) HandleRoute(ctx context.Context, route string) (http.Handler, dflt.Fallback = home.publicHandler(ctx) return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - slug, ok := home.Config.HTTP.SlugFromHost(r.Host) + slug, ok := home.Config.HTTP.NormSlugFromHost(r.Host) switch { case !ok: http.NotFound(w, r) diff --git a/internal/dis/component/server/server.go b/internal/dis/component/server/server.go index c59297b..1dd147f 100644 --- a/internal/dis/component/server/server.go +++ b/internal/dis/component/server/server.go @@ -41,7 +41,7 @@ func (server *Server) Server(ctx context.Context, progress io.Writer) (public ht var publicM, internalM mux.Mux[component.RouteContext] publicM.Context = func(r *http.Request) component.RouteContext { - slug, ok := server.Still.Config.HTTP.SlugFromHost(r.Host) + slug, ok := server.Config.HTTP.NormSlugFromHost(r.Host) return component.RouteContext{ DefaultDomain: slug == "" && ok, } diff --git a/internal/dis/component/server/server/docker-compose.yml b/internal/dis/component/server/server/docker-compose.yml index 46581d2..ddb0e48 100644 --- a/internal/dis/component/server/server/docker-compose.yml +++ b/internal/dis/component/server/server/docker-compose.yml @@ -10,14 +10,14 @@ services: - "traefik.enable=True" - "eu.wiss-ki.barrel.distillery=${DOCKER_NETWORK_NAME}" - - "traefik.http.routers.control.rule=${HOST_RULE}" + - "traefik.http.routers.core_panel.rule=${HOST_RULE}" - - "traefik.http.routers.fallback.rule=HostRegexp(`{catchall:.*}`)" - - "traefik.http.routers.fallback.priority=1" + - "traefik.http.routers.core_fallback.rule=HostRegexp(`{catchall:.*}`)" + - "traefik.http.routers.core_fallback.priority=1" - - "traefik.http.routers.control.tls=${HTTPS_ENABLED}" - - "traefik.http.routers.control.tls.certresolver=distillery" - - "traefik.http.services.control.loadbalancer.server.port=8888" + - "traefik.http.routers.core_panel.tls=${HTTPS_ENABLED}" + - "traefik.http.routers.core_panel.tls.certresolver=distillery" + - "traefik.http.services.core_panel.loadbalancer.server.port=8888" volumes: diff --git a/internal/dis/component/server/stack.go b/internal/dis/component/server/stack.go index 69fc80d..028760a 100644 --- a/internal/dis/component/server/stack.go +++ b/internal/dis/component/server/stack.go @@ -25,7 +25,7 @@ func (server *Server) Stack() component.StackWithResources { EnvContext: map[string]string{ "DOCKER_NETWORK_NAME": server.Config.Docker.Network(), - "HOST_RULE": server.Config.HTTP.DefaultHostRule(), + "HOST_RULE": server.Config.HTTP.PanelHostRule(), "HTTPS_ENABLED": server.Config.HTTP.HTTPSEnabledEnv(), "CONFIG_PATH": server.Config.ConfigPath, diff --git a/internal/dis/component/ssh2/server_handler.go b/internal/dis/component/ssh2/server_handler.go index e6fd798..0119d2d 100644 --- a/internal/dis/component/ssh2/server_handler.go +++ b/internal/dis/component/ssh2/server_handler.go @@ -45,7 +45,7 @@ func (ssh2 *SSH2) handleConnection(session ssh.Session) { {"${SLUG}", slug}, {"${HOSTNAME}", slug + "." + ssh2.Config.HTTP.PrimaryDomain}, - {"${DOMAIN}", ssh2.Config.HTTP.PrimaryDomain}, + {"${DOMAIN}", ssh2.Config.HTTP.PublicTopDomain()}, {"${PORT}", strconv.FormatUint(uint64(ssh2.Config.Listen.SSHPort), 10)}, {"${HELP_URL}", ssh2.Config.HTTP.JoinPath("user", "ssh").String()}, diff --git a/internal/dis/component/ssh2/stack.go b/internal/dis/component/ssh2/stack.go index 7290919..194e203 100644 --- a/internal/dis/component/ssh2/stack.go +++ b/internal/dis/component/ssh2/stack.go @@ -22,7 +22,6 @@ func (ssh *SSH2) Stack() component.StackWithResources { EnvContext: map[string]string{ "DOCKER_NETWORK_NAME": ssh.Config.Docker.Network(), - "HOST_RULE": ssh.Config.HTTP.DefaultHostRule(), "CONFIG_PATH": ssh.Config.ConfigPath, "DEPLOY_ROOT": ssh.Config.Paths.Root,