server: Switch to custom mux
This commit is contained in:
parent
a1069f115e
commit
ab9998881b
13 changed files with 313 additions and 62 deletions
|
|
@ -30,7 +30,7 @@ var (
|
||||||
|
|
||||||
func (auth *Auth) Routes() component.Routes {
|
func (auth *Auth) Routes() component.Routes {
|
||||||
return component.Routes{
|
return component.Routes{
|
||||||
Paths: []string{"/auth/"},
|
Prefix: "/auth/",
|
||||||
CSRF: true,
|
CSRF: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ var (
|
||||||
|
|
||||||
func (next *Next) Routes() component.Routes {
|
func (next *Next) Routes() component.Routes {
|
||||||
return component.Routes{
|
return component.Routes{
|
||||||
Paths: []string{"/next/"},
|
Prefix: "/next/",
|
||||||
Decorator: next.Dependencies.Auth.Require(auth.User),
|
Decorator: next.Dependencies.Auth.Require(auth.User),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ var (
|
||||||
|
|
||||||
func (panel *UserPanel) Routes() component.Routes {
|
func (panel *UserPanel) Routes() component.Routes {
|
||||||
return component.Routes{
|
return component.Routes{
|
||||||
Paths: []string{"/user/"},
|
Prefix: "/user/",
|
||||||
CSRF: true,
|
CSRF: true,
|
||||||
Decorator: panel.Dependencies.Auth.Require(nil),
|
Decorator: panel.Dependencies.Auth.Require(nil),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@ var (
|
||||||
|
|
||||||
func (admin *Admin) Routes() component.Routes {
|
func (admin *Admin) Routes() component.Routes {
|
||||||
return component.Routes{
|
return component.Routes{
|
||||||
Paths: []string{"/admin/"},
|
Prefix: "/admin/",
|
||||||
CSRF: true,
|
CSRF: true,
|
||||||
Decorator: admin.Dependencies.Auth.Require(auth.Admin),
|
Decorator: admin.Dependencies.Auth.Require(auth.Admin),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,8 @@ var (
|
||||||
|
|
||||||
func (*Home) Routes() component.Routes {
|
func (*Home) Routes() component.Routes {
|
||||||
return component.Routes{
|
return component.Routes{
|
||||||
Paths: []string{"/"},
|
Prefix: "/",
|
||||||
|
MatchAllDomains: true,
|
||||||
CSRF: false,
|
CSRF: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,9 @@ var legalTemplate = static.AssetsLegal.MustParseShared("legal.html", legalTempla
|
||||||
|
|
||||||
func (legal *Legal) Routes() component.Routes {
|
func (legal *Legal) Routes() component.Routes {
|
||||||
return component.Routes{
|
return component.Routes{
|
||||||
Paths: []string{"/legal/"},
|
Prefix: "/legal/",
|
||||||
|
Exact: true,
|
||||||
|
|
||||||
CSRF: false,
|
CSRF: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,8 @@ var (
|
||||||
|
|
||||||
func (*News) Routes() component.Routes {
|
func (*News) Routes() component.Routes {
|
||||||
return component.Routes{
|
return component.Routes{
|
||||||
Paths: []string{"/news/"},
|
Prefix: "/news/",
|
||||||
|
Exact: true,
|
||||||
CSRF: false,
|
CSRF: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -126,10 +127,6 @@ func (news *News) HandleRoute(ctx context.Context, path string) (http.Handler, e
|
||||||
|
|
||||||
return httpx.HTMLHandler[newsContext]{
|
return httpx.HTMLHandler[newsContext]{
|
||||||
Handler: func(r *http.Request) (nc newsContext, err error) {
|
Handler: func(r *http.Request) (nc newsContext, err error) {
|
||||||
if strings.TrimSuffix(r.URL.Path, "/") != strings.TrimSuffix(path, "/") {
|
|
||||||
return nc, httpx.ErrNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
news.Dependencies.Custom.Update(&nc, r)
|
news.Dependencies.Custom.Update(&nc, r)
|
||||||
nc.Items, err = items, itemsErr
|
nc.Items, err = items, itemsErr
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,14 @@ package control
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/FAU-CDI/wisski-distillery/internal/dis/component"
|
||||||
"github.com/FAU-CDI/wisski-distillery/pkg/cancel"
|
"github.com/FAU-CDI/wisski-distillery/pkg/cancel"
|
||||||
|
"github.com/FAU-CDI/wisski-distillery/pkg/httpx"
|
||||||
|
"github.com/FAU-CDI/wisski-distillery/pkg/mux"
|
||||||
"github.com/gorilla/csrf"
|
"github.com/gorilla/csrf"
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
)
|
)
|
||||||
|
|
@ -15,8 +19,25 @@ import (
|
||||||
//
|
//
|
||||||
// Logging messages are directed to progress
|
// Logging messages are directed to progress
|
||||||
func (control *Control) Server(ctx context.Context, progress io.Writer) (http.Handler, error) {
|
func (control *Control) Server(ctx context.Context, progress io.Writer) (http.Handler, error) {
|
||||||
// create a new mux
|
logger := zerolog.Ctx(ctx)
|
||||||
mux := http.NewServeMux()
|
|
||||||
|
var mux mux.Mux[component.RouteContext]
|
||||||
|
mux.Context = func(r *http.Request) component.RouteContext {
|
||||||
|
slug, ok := control.Still.Config.SlugFromHost(r.Host)
|
||||||
|
return component.RouteContext{
|
||||||
|
DefaultDomain: slug == "" && ok,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mux.Panic = func(panic any, w http.ResponseWriter, r *http.Request) {
|
||||||
|
// log the panic
|
||||||
|
logger.Error().
|
||||||
|
Str("panic", fmt.Sprint(panic)).
|
||||||
|
Str("path", r.URL.Path).
|
||||||
|
Msg("panic serving handler")
|
||||||
|
|
||||||
|
// and send an internal server error
|
||||||
|
httpx.TextInterceptor.Fallback.ServeHTTP(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
// create a csrf protector
|
// create a csrf protector
|
||||||
csrfProtector := control.CSRF()
|
csrfProtector := control.CSRF()
|
||||||
|
|
@ -24,14 +45,35 @@ func (control *Control) Server(ctx context.Context, progress io.Writer) (http.Ha
|
||||||
// iterate over all the handler
|
// iterate over all the handler
|
||||||
for _, s := range control.Dependencies.Routeables {
|
for _, s := range control.Dependencies.Routeables {
|
||||||
routes := s.Routes()
|
routes := s.Routes()
|
||||||
zerolog.Ctx(ctx).Info().Str("component", s.Name()).Strs("paths", routes.Paths).Bool("csrf", routes.CSRF).Bool("decorator", routes.Decorator != nil).Msg("mounting route")
|
zerolog.Ctx(ctx).Info().
|
||||||
|
Str("Name", s.Name()).
|
||||||
|
Str("Prefix", routes.Prefix).
|
||||||
|
Strs("Aliases", routes.Aliases).
|
||||||
|
Bool("Exact", routes.Exact).
|
||||||
|
Bool("CSRF", routes.CSRF).
|
||||||
|
Bool("Decorator", routes.Decorator != nil).
|
||||||
|
Bool("MatchAllDomains", routes.MatchAllDomains).
|
||||||
|
Msg("mounting route")
|
||||||
|
|
||||||
for _, path := range routes.Paths {
|
// call the handler for the route
|
||||||
handler, err := s.HandleRoute(ctx, path)
|
handler, err := s.HandleRoute(ctx, routes.Prefix)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
zerolog.Ctx(ctx).Err(err).
|
||||||
|
Str("Component", s.Name()).
|
||||||
|
Str("Prefix", routes.Prefix).
|
||||||
|
Msg("error mounting route")
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
mux.Handle(path, routes.Decorate(handler, csrfProtector))
|
|
||||||
|
// decorate the handler
|
||||||
|
handler = routes.Decorate(handler, csrfProtector)
|
||||||
|
|
||||||
|
// determine the predicate
|
||||||
|
predicate := routes.Predicate(mux.ContextOf)
|
||||||
|
|
||||||
|
// and add all the prefixes
|
||||||
|
for _, prefix := range append([]string{routes.Prefix}, routes.Aliases...) {
|
||||||
|
mux.Add(prefix, predicate, routes.Exact, handler)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,8 @@ var (
|
||||||
|
|
||||||
func (*Static) Routes() component.Routes {
|
func (*Static) Routes() component.Routes {
|
||||||
return component.Routes{
|
return component.Routes{
|
||||||
Paths: []string{"/static/"},
|
Prefix: "/static/",
|
||||||
|
|
||||||
CSRF: false,
|
CSRF: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,8 +29,6 @@ type Resolver struct {
|
||||||
|
|
||||||
prefixes lazy.Lazy[map[string]string] // cached prefixes (from the server)
|
prefixes lazy.Lazy[map[string]string] // cached prefixes (from the server)
|
||||||
RefreshInterval time.Duration
|
RefreshInterval time.Duration
|
||||||
|
|
||||||
handler lazy.Lazy[wdresolve.ResolveHandler] // handler
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|
@ -40,7 +38,8 @@ var (
|
||||||
|
|
||||||
func (resolver *Resolver) Routes() component.Routes {
|
func (resolver *Resolver) Routes() component.Routes {
|
||||||
return component.Routes{
|
return component.Routes{
|
||||||
Paths: []string{"/go/", "/wisski/get/"},
|
Prefix: "/wisski/get/",
|
||||||
|
Aliases: []string{"/go/"},
|
||||||
CSRF: false,
|
CSRF: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -59,8 +58,9 @@ func (resolver *Resolver) HandleRoute(ctx context.Context, route string) (http.H
|
||||||
|
|
||||||
logger := zerolog.Ctx(ctx)
|
logger := zerolog.Ctx(ctx)
|
||||||
|
|
||||||
|
var p wdresolve.ResolveHandler
|
||||||
var err error
|
var err error
|
||||||
return resolver.handler.Get(func() (p wdresolve.ResolveHandler) {
|
|
||||||
p.HandleIndex = func(context wdresolve.IndexContext, w http.ResponseWriter, r *http.Request) {
|
p.HandleIndex = func(context wdresolve.IndexContext, w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := resolverContext{
|
ctx := resolverContext{
|
||||||
IndexContext: context,
|
IndexContext: context,
|
||||||
|
|
@ -93,8 +93,7 @@ func (resolver *Resolver) HandleRoute(ctx context.Context, route string) (http.H
|
||||||
resolver,
|
resolver,
|
||||||
fallback,
|
fallback,
|
||||||
}
|
}
|
||||||
return p
|
return p, err
|
||||||
}), err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (resolver *Resolver) Target(uri string) string {
|
func (resolver *Resolver) Target(uri string) string {
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@ package component
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/FAU-CDI/wisski-distillery/pkg/mux"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Routeable is a component that is servable
|
// Routeable is a component that is servable
|
||||||
|
|
@ -18,9 +20,18 @@ type Routeable interface {
|
||||||
|
|
||||||
// Routes represents information about a single Routeable
|
// Routes represents information about a single Routeable
|
||||||
type Routes struct {
|
type Routes struct {
|
||||||
// Paths are the paths handled by this routeable.
|
// Prefix is the prefix this pattern handles
|
||||||
// Each path is passed to HandleRoute() individually.
|
Prefix string
|
||||||
Paths []string
|
|
||||||
|
// MatchAllDomains indicates that all domains, even the non-default domain, should be matched
|
||||||
|
MatchAllDomains bool
|
||||||
|
|
||||||
|
// Exact indicates that only the exact prefix, as opposed to any sub-paths, are matched.
|
||||||
|
// Trailing '/'s are automatically trimmed, even with an exact match.
|
||||||
|
Exact bool
|
||||||
|
|
||||||
|
// Aliases are the additional prefixes this route handles.
|
||||||
|
Aliases []string
|
||||||
|
|
||||||
// CSRF indicates if this route should be protected by CSRF.
|
// CSRF indicates if this route should be protected by CSRF.
|
||||||
// CSRF protection is applied prior to any custom decorator being called.
|
// CSRF protection is applied prior to any custom decorator being called.
|
||||||
|
|
@ -31,6 +42,22 @@ type Routes struct {
|
||||||
Decorator func(http.Handler) http.Handler
|
Decorator func(http.Handler) http.Handler
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type RouteContext struct {
|
||||||
|
DefaultDomain bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Predicate returns the predicate corresponding to the given route
|
||||||
|
func (routes Routes) Predicate(context func(*http.Request) RouteContext) mux.Predicate {
|
||||||
|
if routes.MatchAllDomains {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// match only the default domain
|
||||||
|
return func(r *http.Request) bool {
|
||||||
|
return context(r).DefaultDomain
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Decorate decorates the provided handler with the options specified in this handler.
|
// Decorate decorates the provided handler with the options specified in this handler.
|
||||||
func (routes Routes) Decorate(handler http.Handler, csrf func(http.Handler) http.Handler) http.Handler {
|
func (routes Routes) Decorate(handler http.Handler, csrf func(http.Handler) http.Handler) http.Handler {
|
||||||
if routes.CSRF && csrf != nil {
|
if routes.CSRF && csrf != nil {
|
||||||
|
|
|
||||||
154
pkg/mux/mux.go
Normal file
154
pkg/mux/mux.go
Normal file
|
|
@ -0,0 +1,154 @@
|
||||||
|
// Package mux provides mux
|
||||||
|
package mux
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Mux represents a mux that can handle different requests
|
||||||
|
type Mux[C any] struct {
|
||||||
|
prefixes map[string][]handler
|
||||||
|
exacts map[string][]handler
|
||||||
|
|
||||||
|
Context func(r *http.Request) C // called to set context on the given request
|
||||||
|
|
||||||
|
Panic func(panic any, w http.ResponseWriter, r *http.Request) // called on panic
|
||||||
|
NotFound http.Handler // optional handler to be called in case of a not found
|
||||||
|
}
|
||||||
|
|
||||||
|
type contextKey struct{}
|
||||||
|
|
||||||
|
var theContextKey = contextKey{}
|
||||||
|
|
||||||
|
type handler struct {
|
||||||
|
Predicate Predicate
|
||||||
|
http.Handler
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mux *Mux[T]) Prepare(r *http.Request) *http.Request {
|
||||||
|
if mux == nil || mux.Context == nil {
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := context.WithValue(r.Context(), theContextKey, mux.Context(r))
|
||||||
|
return r.WithContext(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mux *Mux[T]) ContextOf(r *http.Request) (t T) {
|
||||||
|
value, ok := r.Context().Value(theContextKey).(T)
|
||||||
|
if !ok {
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add adds a handler for the given path
|
||||||
|
func (mux *Mux[T]) Add(path string, predicate Predicate, exact bool, h http.Handler) {
|
||||||
|
if mux.exacts == nil {
|
||||||
|
mux.exacts = make(map[string][]handler)
|
||||||
|
}
|
||||||
|
if mux.prefixes == nil {
|
||||||
|
mux.prefixes = make(map[string][]handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
mPath := normalizePath(path)
|
||||||
|
mHandler := handler{Predicate: predicate, Handler: h}
|
||||||
|
if exact {
|
||||||
|
mux.exacts[mPath] = append(mux.exacts[mPath], mHandler)
|
||||||
|
} else {
|
||||||
|
mux.prefixes[mPath] = append(mux.prefixes[mPath], mHandler)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Match returns the handler to be applied for the given request.
|
||||||
|
func (mux *Mux[T]) Match(r *http.Request, prepare bool) (http.Handler, bool) {
|
||||||
|
if mux == nil {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
if prepare {
|
||||||
|
r = mux.Prepare(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
candidate := normalizePath(r.URL.Path)
|
||||||
|
|
||||||
|
// match the exact path first
|
||||||
|
for _, h := range mux.exacts[candidate] {
|
||||||
|
if h.Predicate.Call(r) {
|
||||||
|
return h.Handler, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// iterate over path segment candidates
|
||||||
|
for {
|
||||||
|
// check the current candidate
|
||||||
|
for _, h := range mux.prefixes[candidate] {
|
||||||
|
if h.Predicate.Call(r) {
|
||||||
|
return h.Handler, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if the candidate is the root url, we can bail out now
|
||||||
|
if len(candidate) == 0 || candidate == "/" {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// move to the parent segment
|
||||||
|
candidate = parentSegment(candidate)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mux *Mux[T]) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// handle panics with the panic handler
|
||||||
|
defer func() {
|
||||||
|
caught := recover()
|
||||||
|
if caught == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if mux == nil || mux.Panic == nil {
|
||||||
|
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// silently ignore any panic()s in the panic handler
|
||||||
|
defer func() {
|
||||||
|
recover()
|
||||||
|
}()
|
||||||
|
|
||||||
|
// call the panic handler
|
||||||
|
mux.Panic(caught, w, r)
|
||||||
|
}()
|
||||||
|
|
||||||
|
// prepare the request
|
||||||
|
r = mux.Prepare(r)
|
||||||
|
|
||||||
|
// find the right handler
|
||||||
|
// or go into 404 mode
|
||||||
|
handler, ok := mux.Match(r, false)
|
||||||
|
if !ok {
|
||||||
|
if mux == nil || mux.NotFound == nil {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
mux.NotFound.ServeHTTP(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// call the actual handling
|
||||||
|
handler.ServeHTTP(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Predicate represents a matching predicate for a given request.
|
||||||
|
// The nil predicate always matches
|
||||||
|
type Predicate func(r *http.Request) bool
|
||||||
|
|
||||||
|
// Call checks if this predicate matches the given request.
|
||||||
|
func (p Predicate) Call(r *http.Request) bool {
|
||||||
|
if p == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return p(r)
|
||||||
|
}
|
||||||
28
pkg/mux/path.go
Normal file
28
pkg/mux/path.go
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
package mux
|
||||||
|
|
||||||
|
import (
|
||||||
|
"path"
|
||||||
|
)
|
||||||
|
|
||||||
|
// normalizePath normalizes the provided path.
|
||||||
|
// It ensures that there is both a leading and trailing slash.
|
||||||
|
func normalizePath(value string) string {
|
||||||
|
value = path.Clean(value)
|
||||||
|
if value != "/" {
|
||||||
|
value = value + "/"
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
// parentSegment returns the parent segment of the provided path
|
||||||
|
// it assumes that normalizePath has been called on value.
|
||||||
|
func parentSegment(value string) string {
|
||||||
|
if value == "" || value == "/" {
|
||||||
|
return "/"
|
||||||
|
}
|
||||||
|
parent := path.Dir(value[:len(value)-1])
|
||||||
|
if parent != "/" {
|
||||||
|
parent = parent + "/"
|
||||||
|
}
|
||||||
|
return parent
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue