Skip to content

Commit

Permalink
Add custom domain for local resolution
Browse files Browse the repository at this point in the history
  • Loading branch information
kiootic committed Nov 28, 2023
1 parent dddba87 commit 9746b44
Show file tree
Hide file tree
Showing 23 changed files with 195 additions and 56 deletions.
7 changes: 5 additions & 2 deletions cmd/controller/app/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"github.com/oursky/pageship/internal/db"
_ "github.com/oursky/pageship/internal/db/postgres"
_ "github.com/oursky/pageship/internal/db/sqlite"
"github.com/oursky/pageship/internal/domain"
"github.com/oursky/pageship/internal/handler/controller"
"github.com/oursky/pageship/internal/handler/site"
"github.com/oursky/pageship/internal/handler/site/middleware"
Expand Down Expand Up @@ -128,15 +129,17 @@ func (s *setup) checkDomain(name string) error {
}

func (s *setup) sites(conf StartSitesConfig) error {
resolver := &sitedb.Resolver{
domainResolver := &domain.ResolverNull{} // FIXME: custom domain
siteResolver := &sitedb.Resolver{
HostIDScheme: conf.HostIDScheme,
DB: s.database,
Storage: s.storage,
}
handler, err := site.NewHandler(
s.ctx,
logger.Named("site"),
resolver,
domainResolver,
siteResolver,
site.HandlerConfig{
HostPattern: conf.HostPattern,
Middlewares: middleware.Default,
Expand Down
32 changes: 21 additions & 11 deletions cmd/pageship/app/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,13 @@ import (
"github.com/caddyserver/certmagic"
"github.com/oursky/pageship/internal/command"
"github.com/oursky/pageship/internal/config"
"github.com/oursky/pageship/internal/domain"
domainlocal "github.com/oursky/pageship/internal/domain/local"
handler "github.com/oursky/pageship/internal/handler/site"
"github.com/oursky/pageship/internal/handler/site/middleware"
"github.com/oursky/pageship/internal/httputil"
"github.com/oursky/pageship/internal/site"
"github.com/oursky/pageship/internal/site/local"
sitelocal "github.com/oursky/pageship/internal/site/local"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
Expand Down Expand Up @@ -53,11 +55,13 @@ func makeHandler(prefix string, defaultSite string, hostPattern string) (*handle

fsys := os.DirFS(dir)

var resolver site.Resolver
resolver = local.NewSingleSiteResolver(fsys)
var siteResolver site.Resolver
siteResolver = sitelocal.NewSingleSiteResolver(fsys)
var domainResolver domain.Resolver
domainResolver = &domain.ResolverNull{}

// Check site on startup.
_, err = resolver.Resolve(context.Background(), defaultSite)
_, err = siteResolver.Resolve(context.Background(), defaultSite)
if errors.Is(err, config.ErrConfigNotFound) {
// continue in multi-site mode

Expand All @@ -72,17 +76,23 @@ func makeHandler(prefix string, defaultSite string, hostPattern string) (*handle
if sitesConf != nil {
sites = sitesConf.Sites
}
resolver = local.NewMultiSiteResolver(fsys, defaultSite, sites)
siteResolver = sitelocal.NewResolver(fsys, defaultSite, sites)
domainResolver, err = domainlocal.NewResolver(defaultSite, sites)
if err != nil {
return nil, err
}
} else if err != nil {
return nil, err
}

Info("site resolution mode: %s", resolver.Kind())
Info("site resolution mode: %s", siteResolver.Kind())

handler, err := handler.NewHandler(context.Background(), zapLogger, resolver, handler.HandlerConfig{
HostPattern: hostPattern,
Middlewares: middleware.Default,
})
handler, err := handler.NewHandler(context.Background(), zapLogger,
domainResolver, siteResolver,
handler.HandlerConfig{
HostPattern: hostPattern,
Middlewares: middleware.Default,
})
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -127,7 +137,7 @@ var serveCmd = &cobra.Command{

if len(tlsDomain) > 0 {
tls.DomainNames = []string{tlsDomain}
} else if handler.AllowAnyDomain() {
} else if handler.AcceptsAllDomain() {
return fmt.Errorf("must provide domain name via --tls-domain to enable TLS")
}
}
Expand Down
8 changes: 6 additions & 2 deletions examples/sites.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
[sites."main"]
context="main"
context = "main"

[sites."dev"]
context="dev"
context = "dev"

[sites."custom"]
context = "dev"
domain = "example.com"
1 change: 1 addition & 0 deletions internal/config/sites.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ type SitesConfig struct {

type SitesConfigEntry struct {
Context string `json:"context"`
Domain string `json:"domain"`
}

func DefaultSitesConfig() *SitesConfig {
Expand Down
15 changes: 15 additions & 0 deletions internal/domain/local/resolver.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package local

import (
"github.com/oursky/pageship/internal/config"
"github.com/oursky/pageship/internal/domain"
)

func NewResolver(defaultSite string, sites map[string]config.SitesConfigEntry) (domain.Resolver, error) {
if len(sites) == 0 {
// Custom domain resolution is not supported for ad-hoc sites.
return &domain.ResolverNull{}, nil
}

return newResolverStatic(defaultSite, sites)
}
44 changes: 44 additions & 0 deletions internal/domain/local/resolver_static.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package local

import (
"context"
"fmt"

"github.com/oursky/pageship/internal/config"
"github.com/oursky/pageship/internal/domain"
)

type resolverStatic struct {
domains map[string]string
}

func newResolverStatic(defaultSite string, sites map[string]config.SitesConfigEntry) (*resolverStatic, error) {
domains := make(map[string]string)
for id, site := range sites {
if site.Domain == "" {
continue
}

if _, exists := domains[site.Domain]; exists {
return nil, fmt.Errorf("duplicated domain: %q", site.Domain)
}

if defaultSite != "-" && id == defaultSite {
id = ""
}
domains[site.Domain] = id
}

return &resolverStatic{domains: domains}, nil
}

func (h *resolverStatic) Kind() string { return "static config" }

func (h *resolverStatic) Resolve(ctx context.Context, hostname string) (string, error) {
id, ok := h.domains[hostname]
if !ok {
return "", domain.ErrDomainNotFound
}

return id, nil
}
13 changes: 13 additions & 0 deletions internal/domain/resolver.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package domain

import (
"context"
"errors"
)

var ErrDomainNotFound = errors.New("domain not found")

type Resolver interface {
Kind() string
Resolve(ctx context.Context, hostname string) (string, error)
}
13 changes: 13 additions & 0 deletions internal/domain/resolver_null.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package domain

import (
"context"
)

type ResolverNull struct{}

func (h *ResolverNull) Kind() string { return "null" }

func (h *ResolverNull) Resolve(ctx context.Context, hostname string) (string, error) {
return "", ErrDomainNotFound
}
59 changes: 34 additions & 25 deletions internal/handler/site/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/go-chi/chi/v5/middleware"
"github.com/oursky/pageship/internal/cache"
"github.com/oursky/pageship/internal/config"
"github.com/oursky/pageship/internal/domain"
"github.com/oursky/pageship/internal/httputil"
"github.com/oursky/pageship/internal/models"
"github.com/oursky/pageship/internal/site"
Expand All @@ -28,21 +29,23 @@ type HandlerConfig struct {
}

type Handler struct {
ctx context.Context
logger *zap.Logger
resolver site.Resolver
hostPattern *config.HostPattern
cache *cache.Cache[*SiteHandler]
middlewares []Middleware
ctx context.Context
logger *zap.Logger
domainResolver domain.Resolver
siteResolver site.Resolver
hostPattern *config.HostPattern
cache *cache.Cache[*SiteHandler]
middlewares []Middleware
}

func NewHandler(ctx context.Context, logger *zap.Logger, resolver site.Resolver, conf HandlerConfig) (*Handler, error) {
func NewHandler(ctx context.Context, logger *zap.Logger, domainResolver domain.Resolver, siteResolver site.Resolver, conf HandlerConfig) (*Handler, error) {
h := &Handler{
ctx: ctx,
logger: logger,
resolver: resolver,
hostPattern: config.NewHostPattern(conf.HostPattern),
middlewares: conf.Middlewares,
ctx: ctx,
logger: logger,
domainResolver: domainResolver,
siteResolver: siteResolver,
hostPattern: config.NewHostPattern(conf.HostPattern),
middlewares: conf.Middlewares,
}

cache, err := cache.NewCache(cacheSize, cacheTTL, h.doResolve)
Expand All @@ -54,33 +57,39 @@ func NewHandler(ctx context.Context, logger *zap.Logger, resolver site.Resolver,
return h, nil
}

func (h *Handler) resolveSite(host string) (*SiteHandler, error) {
matchedID, ok := h.hostPattern.MatchString(host)
func (h *Handler) resolveSite(hostname string) (*SiteHandler, error) {
return h.cache.Load(hostname)
}

func (h *Handler) doResolve(hostname string) (*SiteHandler, error) {
matchedID, ok := h.hostPattern.MatchString(hostname)
if !ok {
return nil, site.ErrSiteNotFound
id, err := h.domainResolver.Resolve(h.ctx, hostname)
if errors.Is(err, domain.ErrDomainNotFound) {
return nil, site.ErrSiteNotFound
} else if err != nil {
return nil, err
}
matchedID = id
}

return h.cache.Load(matchedID)
}

func (h *Handler) doResolve(matchedID string) (*SiteHandler, error) {
desc, err := h.resolver.Resolve(h.ctx, matchedID)
desc, err := h.siteResolver.Resolve(h.ctx, matchedID)
if err != nil {
return nil, err
}

return NewSiteHandler(desc, h.middlewares), nil
}

func (h *Handler) AllowAnyDomain() bool {
return h.resolver.AllowAnyDomain()
func (h *Handler) AcceptsAllDomain() bool {
return h.siteResolver.IsWildcard()
}

func (h *Handler) CheckValidDomain(name string) error {
if h.resolver.AllowAnyDomain() {
func (h *Handler) CheckValidDomain(hostname string) error {
if h.siteResolver.IsWildcard() {
return nil
}
_, err := h.resolveSite(name)
_, err := h.resolveSite(hostname)
return err
}

Expand Down
6 changes: 3 additions & 3 deletions internal/handler/site/middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ import (
"github.com/oursky/pageship/internal/site"
)

type Middleware func(site.FS, http.Handler) http.Handler
type Middleware func(*site.Descriptor, http.Handler) http.Handler

func applyMiddleware(fs site.FS, middlewares []Middleware, handler http.Handler) http.Handler {
func applyMiddleware(site *site.Descriptor, middlewares []Middleware, handler http.Handler) http.Handler {
for i := len(middlewares) - 1; i >= 0; i-- {
handler = middlewares[i](fs, handler)
handler = middlewares[i](site, handler)
}
return handler
}
4 changes: 2 additions & 2 deletions internal/handler/site/middleware/canon.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ import (
"github.com/oursky/pageship/internal/site"
)

func CanonicalizePath(fs site.FS, next http.Handler) http.Handler {
func CanonicalizePath(site *site.Descriptor, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
urlpath, err := canonicalizePath(fs, r.URL.Path)
urlpath, err := canonicalizePath(site.FS, r.URL.Path)
if err != nil {
handler.Error(w, r, err)
return
Expand Down
20 changes: 20 additions & 0 deletions internal/handler/site/middleware/custom_domain.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package middleware

import (
"net/http"

"github.com/oursky/pageship/internal/site"
)

func RedirectCustomDomain(site *site.Descriptor, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if site.Domain != "" && r.Host != site.Domain {
// Site with custom domain must be accessed through the custom domain.
url := *r.URL
url.Host = site.Domain
http.Redirect(w, r, url.String(), http.StatusMovedPermanently)
return
}
next.ServeHTTP(w, r)
})
}
4 changes: 2 additions & 2 deletions internal/handler/site/middleware/index.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ import (
"github.com/oursky/pageship/internal/site"
)

func IndexPage(fs site.FS, next http.Handler) http.Handler {
func IndexPage(site *site.Descriptor, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
const indexPage = "index.html"

info, err := fs.Stat(r.URL.Path)
info, err := site.FS.Stat(r.URL.Path)
if err != nil {
http.Error(w, "internal server error", http.StatusInternalServerError)
return
Expand Down
1 change: 1 addition & 0 deletions internal/handler/site/middleware/middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package middleware
import "github.com/oursky/pageship/internal/handler/site"

var Default = []site.Middleware{
RedirectCustomDomain,
CanonicalizePath,
RouteSPA,
IndexPage,
Expand Down
4 changes: 2 additions & 2 deletions internal/handler/site/middleware/spa.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ import (
)

// RouteSPA routes non-existing files to nearest parent directory
func RouteSPA(fs site.FS, next http.Handler) http.Handler {
func RouteSPA(site *site.Descriptor, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
urlpath := r.URL.Path
for {
_, err := fs.Stat(urlpath)
_, err := site.FS.Stat(urlpath)
if os.IsNotExist(err) {
if urlpath == "/" {
// Reached root; stop
Expand Down
4 changes: 3 additions & 1 deletion internal/handler/site/site_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ func NewSiteHandler(desc *site.Descriptor, middlewares []Middleware) *SiteHandle
publicFS: site.SubFS(desc.FS, path.Clean("/"+desc.Config.Public)),
}

h.next = applyMiddleware(h.publicFS, middlewares, http.HandlerFunc(h.serveFile))
publicDesc := *desc
publicDesc.FS = site.SubFS(desc.FS, path.Clean("/"+desc.Config.Public))
h.next = applyMiddleware(&publicDesc, middlewares, http.HandlerFunc(h.serveFile))
return h
}

Expand Down
Loading

0 comments on commit 9746b44

Please sign in to comment.