wisski-cloud-distillery/internal/config/http.go
2023-04-28 10:25:36 +02:00

200 lines
5.7 KiB
Go

package config
import (
"fmt"
"net/http"
"net/url"
"strings"
"github.com/FAU-CDI/wisski-distillery/internal/config/validators"
"github.com/tkw1536/pkglib/httpx"
"golang.org/x/net/idna"
)
type HTTPConfig struct {
// Each created Drupal Instance corresponds to a single domain name.
// These domain names should either be a complete domain name or a sub-domain of a default domain.
// This setting configures the default domain-name to create subdomains of.
PrimaryDomain string `yaml:"domain" default:"localhost.kwarc.info" validate:"domain"`
// By default, only the 'self' domain above is caught.
// To catch additional domains, add them here (comma separated)
ExtraDomains []string `yaml:"domains" validate:"domains"`
// The system can support setting up certificate(s) automatically.
// It can be enabled by setting an email for certbot certificates.
// This email address can be configured here.
CertbotEmail string `yaml:"certbot_email" validate:"email"`
// 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"`
}
var apiNotEnabled = httpx.Response{
StatusCode: http.StatusForbidden,
Body: []byte(`{"message":"API is not enabled"}`),
}
func (hcfg HTTPConfig) APIDecorator(methods ...string) func(http.Handler) http.Handler {
methods = append(methods, "OPTIONS") // always permit the options method!
if !hcfg.API.Value {
return func(http.Handler) http.Handler {
return httpx.PermitMethods(apiNotEnabled, methods...)
}
}
// permit only the specified methods
return func(h http.Handler) http.Handler {
return httpx.PermitMethods(h, methods...)
}
}
// JoinPath returns the root public url joined with the provided parts.
func (hcfg HTTPConfig) JoinPath(elem ...string) *url.URL {
u := url.URL{
Scheme: "http",
Host: hcfg.PrimaryDomain,
Path: "/",
}
if hcfg.HTTPSEnabled() {
u.Scheme = "https"
}
return u.JoinPath(elem...)
}
// TCPMuxCommand generates a command line for the sslh executable.
func (hcfg HTTPConfig) TCPMuxCommand(addr string, http string, https string, ssh string) string {
if hcfg.HTTPSEnabled() {
return fmt.Sprintf("-bind %s -http %s -tls %s -rest %s", addr, http, https, ssh)
}
return fmt.Sprintf("-bind %s -http %s -rest %s", addr, http, ssh)
}
// HTTPSEnabled returns if the distillery has HTTPS enabled, and false otherwise.
func (hcfg HTTPConfig) HTTPSEnabled() bool {
return hcfg.CertbotEmail != ""
}
type SpecialDomain string
var (
TriplestoreDomain SpecialDomain = "ts"
)
func (hcfg HTTPConfig) SpecialDomain(domain SpecialDomain) string {
return fmt.Sprintf("%s.%s", string(domain)+"_", hcfg.PrimaryDomain)
}
// Domains adds the given subdomain to the primary and alias domains.
// If sub is empty, returns only the domains.
//
// sub is not otherwise validated, and should be normalized by the caller.
func (hcfg HTTPConfig) Domains(sub string) []string {
domains := append([]string{hcfg.PrimaryDomain}, hcfg.ExtraDomains...)
if sub == "" {
return domains
}
for i, d := range domains {
domains[i] = sub + "." + d
}
return domains
}
// HostRule returns a HostRule for the provided subdomain.
// See Domains() for usage of sub.
func (hcfg HTTPConfig) HostRule(sub string) string {
return MakeHostRule(hcfg.Domains(sub)...)
}
// HTTPSEnabledEnv returns "true" if https is enabled, and "false" otherwise.
func (hcfg HTTPConfig) HTTPSEnabledEnv() string {
if hcfg.HTTPSEnabled() {
return "true"
}
return "false"
}
// HostFromSlug returns the hostname belonging to a given slug.
// When the slug is empty, returns the default (top-level) domain.
func (cfg HTTPConfig) HostFromSlug(slug string) string {
if slug == "" {
return cfg.PrimaryDomain
}
return fmt.Sprintf("%s.%s", slug, cfg.PrimaryDomain)
}
// 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 HTTPConfig) SlugFromHost(host string) (slug string, ok bool) {
// extract an ':port' that happens to be in the host.
domain, _, _ := strings.Cut(host, ":")
domain = TrimSuffixFold(domain, ".wisski") // remove optional ".wisski" ending that is used inside docker
domainL := strings.ToLower(domain)
// check all the possible domain endings
for _, suffix := range append([]string{cfg.PrimaryDomain}, cfg.ExtraDomains...) {
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 "", ok
}
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)]
}
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("")
}
// MakeHostRule builds a new Host() rule string to be used by traefik.
func MakeHostRule(hosts ...string) string {
var builder strings.Builder
first := true
for _, host := range hosts {
// HACK HACK HACK: Very minimal domain validation to prevent validation.
// Just skip everything that isn't a domain.
if strings.Contains(host, "`") {
continue
}
if first {
builder.WriteString("Host(`")
} else {
builder.WriteString("||Host(`")
}
// domain should be punycode!
domain, err := idna.ToASCII(host)
if err != nil {
domain = host
}
builder.WriteString(domain)
builder.WriteString("`)")
first = false
}
return builder.String()
}