clone

a self-hosted multitenant nostr relay with sites, file storage, git hosting, and inbox and outbox delivery

feat/ui-updates
tinyrelay / internal / daemon / routing.go
routing.go 4.2K
1package daemon23import (4 "context"5 "errors"6 "fmt"7 "net"8 "net/http"9 "net/url"10 "strings"1112 "github.com/FelineStateMachine/tinyrelay/internal/catalog"13 "github.com/FelineStateMachine/tinyrelay/internal/sites"14)1516// siteBase returns the tenant-scoped site hostname. A tenant named alice on17// relay.example has sites at <site>.alice.relay.example, keeping identical18// site labels on different tenants isolated.19func (a *App) siteBase(meta catalog.Tenant) string {20 base := "localhost"21 if a.cfg.PublicURL != "" {22 if u, err := url.Parse(a.cfg.PublicURL); err == nil && u.Hostname() != "" {23 base = u.Hostname()24 }25 }26 if ip := net.ParseIP(base); ip != nil {27 return meta.Name + "." + base28 }29 return meta.Name + "." + strings.TrimSuffix(strings.ToLower(base), ".")30}3132// resolveHostedSite resolves a site host without opening a tenant. The bool33// distinguishes a relay host from a site host, while errors preserve catalog34// failures for callers that should not fall back to the default tenant.35func (a *App) resolveHostedSite(r *http.Request) (catalog.Tenant, string, bool, error) {36 host := strings.ToLower(r.Host)37 if h, _, err := net.SplitHostPort(host); err == nil {38 host = h39 }40 if strings.HasPrefix(r.URL.Path, "/r/") {41 parts := strings.Split(strings.TrimPrefix(r.URL.Path, "/r/"), "/")42 if len(parts) > 0 && parts[0] != "" {43 meta, err := a.catalog.GetByName(r.Context(), parts[0])44 return meta, "", false, err45 }46 }47 if mapping, err := a.catalog.ResolveHostMapping(r.Context(), host); err == nil {48 meta, getErr := a.catalog.GetByID(r.Context(), mapping.TenantID)49 if getErr != nil {50 return catalog.Tenant{}, "", false, getErr51 }52 if mapping.Site == "" {53 return meta, "", false, nil54 }55 if _, ok := sites.ParseSite(mapping.Site); !ok {56 return catalog.Tenant{}, mapping.Site, true, errors.New("invalid mapped site label")57 }58 return meta, mapping.Site, true, nil59 } else if !errors.Is(err, catalog.ErrHostMappingNotFound) {60 return catalog.Tenant{}, "", false, err61 }62 base := strings.TrimSuffix(strings.ToLower(a.siteBaseHost()), ".")63 suffix := "." + base64 if !strings.HasSuffix(host, suffix) {65 return catalog.Tenant{}, "", false, catalog.ErrNotFound66 }67 relative := strings.TrimSuffix(strings.TrimSuffix(host, suffix), ".")68 parts := strings.Split(relative, ".")69 if len(parts) == 1 {70 if _, ok := sites.ParseSite(relative); !ok {71 return catalog.Tenant{}, "", false, catalog.ErrNotFound72 }73 meta, err := a.catalog.GetByName(r.Context(), a.cfg.DefaultTenant)74 return meta, relative, true, err75 }76 if len(parts) < 2 {77 return catalog.Tenant{}, "", false, catalog.ErrNotFound78 }79 label := strings.Join(parts[:len(parts)-1], ".")80 if _, ok := sites.ParseSite(label); !ok {81 return catalog.Tenant{}, label, true, catalog.ErrNotFound82 }83 tenantName := parts[len(parts)-1]84 meta, err := a.catalog.GetByName(r.Context(), tenantName)85 if err != nil {86 return catalog.Tenant{}, label, true, err87 }88 return meta, label, true, nil89}9091func (a *App) siteBaseHost() string {92 if a.cfg.PublicURL == "" {93 return "localhost"94 }95 u, err := url.Parse(a.cfg.PublicURL)96 if err != nil || u.Hostname() == "" {97 return "localhost"98 }99 return u.Hostname()100}101102// siteHostResolver is injected into a tenant's sites service. It only returns103// labels assigned to that tenant, preventing a custom host or namespace host104// from crossing tenant boundaries.105func (t *Tenant) siteHostResolver(ctx context.Context, host string) (string, error) {106 host = strings.ToLower(host)107 if h, _, err := net.SplitHostPort(host); err == nil {108 host = h109 }110 if mapping, err := t.app.catalog.ResolveHostMapping(ctx, host); err == nil {111 if mapping.TenantID != t.meta.ID || mapping.Site == "" {112 return "", catalog.ErrHostMappingNotFound113 }114 return mapping.Site, nil115 } else if !errors.Is(err, catalog.ErrHostMappingNotFound) {116 return "", err117 }118 base := "." + strings.ToLower(t.app.siteBase(t.meta))119 if !strings.HasSuffix(host, base) {120 return "", catalog.ErrNotFound121 }122 label := strings.TrimSuffix(strings.TrimSuffix(host, base), ".")123 if _, ok := sites.ParseSite(label); !ok {124 return "", catalog.ErrNotFound125 }126 return label, nil127}128129func (a *App) hostedSiteURL(meta catalog.Tenant, label string) string {130 if label == "" {131 return ""132 }133 return fmt.Sprintf("https://%s/%s", a.siteBase(meta), label)134}