// Package webendpoints provides helpers for composing web-endpoint hostnames.
//
// A web endpoint is a named HTTP tunnel that ShellHub exposes on behalf of a
// device. Each endpoint is reachable at an address that is formed by joining
// an address token (typically a short random slug) and a domain:
//
//
. e.g. abc123.tunnel.example.com
//
// The domain itself can be configured at two levels: a per-namespace
// "preferred" override and a system-wide fallback. The helpers in this
// package centralise that logic so that every caller (nginx template
// generation, API responses, agent-side URL construction) derives the hostname
// the same way.
//
// Note: the nginx-template duplication is by design — keeping the template and
// the Go logic in sync is easier than introducing an indirection layer.
package webendpoints
import (
"net"
"regexp"
"strings"
)
// Domain returns the effective domain for a web endpoint.
//
// When preferred is non-empty it is returned as-is, giving namespace
// administrators the ability to customise the domain without changing the
// system-wide default. Otherwise fallback (the system-level default) is
// returned. Both values may be empty strings; callers must handle that case.
func Domain(preferred, fallback string) string {
if preferred != "" {
return preferred
}
return fallback
}
// ProxyPath is the server route a request addressed to a web endpoint is rewritten to before it
// is carried into the device. The response is the device's own, so no API schema describes it.
const ProxyPath = "/http/proxy"
var address = regexp.MustCompile(`^[a-f0-9]{32}$`)
// AddressFromHost is the inverse of Host: it takes the Host of an inbound
// request and reports the address it names, or false when the request is not
// addressed to a web endpoint at all.
//
// Rejecting a host that only looks close enough is the point. Whoever the
// address resolves to gets the request proxied into their device, so a caller
// must be able to tell "this is a web endpoint" apart from "this is the
// console" without consulting the database first.
//
// The comparison is case-insensitive and tolerates a trailing dot: a Host may
// arrive as a fully qualified name, with the root label spelled out, and still
// be the same name.
func AddressFromHost(host, domain string) (string, bool) {
if domain == "" {
return "", false
}
if hostname, _, err := net.SplitHostPort(host); err == nil {
host = hostname
}
host = strings.ToLower(strings.TrimSuffix(host, "."))
candidate, found := strings.CutSuffix(host, "."+strings.ToLower(domain))
if !found || !address.MatchString(candidate) {
return "", false
}
return candidate, true
}
// Host builds the full hostname for a web endpoint.
//
// The result is "." when domain is non-empty, or just
// "" when domain is empty. The trailing-dot regression guard
// ensures that an empty domain never produces a string ending in ".".
func Host(address, domain string) string {
if domain == "" {
return address
}
return address + "." + domain
}