♻️ Refactored redirection support into middleware

This commit is contained in:
Maxim Lebedev 2024-01-27 13:29:08 +06:00
parent 25494915ba
commit 7a0727aa8d
Signed by: toby3d
GPG Key ID: 1F14E25B7C119FC5
4 changed files with 110 additions and 72 deletions

View File

@ -57,7 +57,7 @@ func NewApp(logger *log.Logger, config *domain.Config) (*App, error) {
themer := themeucase.NewThemeUseCase(partialsDir, themes) themer := themeucase.NewThemeUseCase(partialsDir, themes)
pages := pagefsrepo.NewFileSystemPageRepository(contentDir) pages := pagefsrepo.NewFileSystemPageRepository(contentDir)
pager := pageucase.NewPageUseCase(pages, resources) pager := pageucase.NewPageUseCase(pages, resources)
server := servercase.NewServerUseCase() serverer := servercase.NewServerUseCase()
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// INFO(toby3d): any static file is public and unprotected by design, so it's safe to search it // INFO(toby3d): any static file is public and unprotected by design, so it's safe to search it
// first before deep down to any page or it's resource which might be secured by middleware or // first before deep down to any page or it's resource which might be secured by middleware or
@ -77,7 +77,7 @@ func NewApp(logger *log.Logger, config *domain.Config) (*App, error) {
return return
} }
siteServer, err := server.Do(r.Context(), *s) siteServer, err := serverer.Do(r.Context(), *s)
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
@ -94,26 +94,8 @@ func NewApp(logger *log.Logger, config *domain.Config) (*App, error) {
} }
} }
var redirect *domain.Redirect
for i := range siteServer.Redirects {
if !siteServer.Redirects[i].IsMatch(r.URL.Path) {
continue
}
if siteServer.Redirects[i].Force {
http.Redirect(w, r, siteServer.Redirects[i].To, siteServer.Redirects[i].Status)
return
}
redirect = &siteServer.Redirects[i]
break
}
if s.IsMultiLingual() { if s.IsMultiLingual() {
head, tail := urlutil.ShiftPath(r.URL.Path) head, tail := urlutil.ShiftPath(r.URL.Path)
if head == "" { if head == "" {
supported := make([]language.Tag, len(s.Languages)) supported := make([]language.Tag, len(s.Languages))
for i := range s.Languages { for i := range s.Languages {
@ -125,8 +107,8 @@ func NewApp(logger *log.Logger, config *domain.Config) (*App, error) {
supported...) supported...)
} }
requested, _, err := language.ParseAcceptLanguage( requested, _, err := language.ParseAcceptLanguage(r.Header.Get(
r.Header.Get(common.HeaderAcceptLanguage)) common.HeaderAcceptLanguage))
if err != nil || len(requested) == 0 { if err != nil || len(requested) == 0 {
requested = append(requested, language.English) requested = append(requested, language.English)
} }
@ -139,14 +121,9 @@ func NewApp(logger *log.Logger, config *domain.Config) (*App, error) {
return return
} }
lang = domain.NewLanguage(head) if lang = domain.NewLanguage(head); lang != domain.LanguageUnd {
r.URL.Path = tail r.URL.Path = tail
} }
if lang == domain.LanguageUnd && redirect != nil {
http.Redirect(w, r, redirect.To, redirect.Status)
return
} }
if s, err = siter.Do(r.Context(), lang); err != nil { if s, err = siter.Do(r.Context(), lang); err != nil {
@ -155,7 +132,7 @@ func NewApp(logger *log.Logger, config *domain.Config) (*App, error) {
return return
} }
if siteServer, err = server.Do(r.Context(), *s); err != nil { if siteServer, err = serverer.Do(r.Context(), *s); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
@ -171,22 +148,6 @@ func NewApp(logger *log.Logger, config *domain.Config) (*App, error) {
} }
} }
for i := range siteServer.Redirects {
if !siteServer.Redirects[i].IsMatch(r.URL.Path) {
continue
}
if siteServer.Redirects[i].Force {
http.Redirect(w, r, siteServer.Redirects[i].To, siteServer.Redirects[i].Status)
return
}
redirect = &siteServer.Redirects[i]
break
}
p, err := pager.Do(r.Context(), lang, r.URL.Path) p, err := pager.Do(r.Context(), lang, r.URL.Path)
if err != nil { if err != nil {
if !errors.Is(err, page.ErrNotExist) { if !errors.Is(err, page.ErrNotExist) {
@ -195,12 +156,6 @@ func NewApp(logger *log.Logger, config *domain.Config) (*App, error) {
return return
} }
if redirect != nil {
http.Redirect(w, r, redirect.To, redirect.Status)
return
}
res, err := resourcer.Do(r.Context(), r.URL.Path) res, err := resourcer.Do(r.Context(), r.URL.Path)
if err != nil { if err != nil {
if errors.Is(err, fs.ErrNotExist) { if errors.Is(err, fs.ErrNotExist) {
@ -255,7 +210,13 @@ func NewApp(logger *log.Logger, config *domain.Config) (*App, error) {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
} }
}) })
chain := middleware.Chain{middleware.LogFmt()} chain := middleware.Chain{
middleware.LogFmt(),
middleware.Redirect(middleware.RedirectConfig{
Siter: siter,
Serverer: serverer,
}),
}
return &App{server: &http.Server{ return &App{server: &http.Server{
Addr: config.AddrPort().String(), Addr: config.AddrPort().String(),

View File

@ -0,0 +1,15 @@
package domain
type Redirects []Redirect
func (r Redirects) Match(p string) (*Redirect, bool) {
for i := range r {
if !r[i].IsMatch(p) {
continue
}
return &r[i], true
}
return nil, false
}

View File

@ -2,12 +2,12 @@ package domain
type Server struct { type Server struct {
Headers []Header Headers []Header
Redirects []Redirect Redirects Redirects
} }
func NewServer() *Server { func NewServer() *Server {
return &Server{ return &Server{
Headers: make([]Header, 0), Headers: make([]Header, 0),
Redirects: make([]Redirect, 0), Redirects: make(Redirects, 0),
} }
} }

View File

@ -2,25 +2,38 @@ package middleware
import ( import (
"net/http" "net/http"
"net/url"
"source.toby3d.me/toby3d/home/internal/domain"
"source.toby3d.me/toby3d/home/internal/server"
"source.toby3d.me/toby3d/home/internal/site"
"source.toby3d.me/toby3d/home/internal/urlutil"
) )
type ( type (
RedirectConfig struct { RedirectConfig struct {
Skipper Skipper Skipper Skipper
Code int Siter site.UseCase
Serverer server.UseCase
} }
redirectLogic func(u *url.URL) (url string, ok bool) redirectResponse struct {
http.ResponseWriter
error error
statusCode int
}
) )
func Redirect(config RedirectConfig, redirect redirectLogic) Interceptor { func Redirect(config RedirectConfig) Interceptor {
if config.Skipper == nil { if config.Skipper == nil {
config.Skipper = DefaultSkipper config.Skipper = DefaultSkipper
} }
if config.Code == 0 { if config.Siter == nil {
config.Code = http.StatusMovedPermanently panic("middleware: redirect: Siter is nil")
}
if config.Serverer == nil {
panic("middleware: redirect: Serverer is nil")
} }
return func(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) { return func(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
@ -30,20 +43,69 @@ func Redirect(config RedirectConfig, redirect redirectLogic) Interceptor {
return return
} }
u := &url.URL{ lang, path := domain.LanguageUnd, r.URL.Path
Scheme: "http", if head, tail := urlutil.ShiftPath(r.URL.Path); head != "" {
Host: r.Host, if lang = domain.NewLanguage(head); lang != domain.LanguageUnd {
Path: r.RequestURI, path = tail
}
} }
if r.TLS != nil { site, err := config.Siter.Do(r.Context(), lang)
u.Scheme += "s" if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
} }
if target, ok := redirect(u); ok { server, err := config.Serverer.Do(r.Context(), *site)
http.RedirectHandler(target, config.Code).ServeHTTP(w, r) if err != nil {
} else { http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
redirect, ok := server.Redirects.Match(path)
if !ok {
next(w, r) next(w, r)
return
} }
// NOTE(toby3d): always redirect no matter what exists on
// requested URL.
if redirect.Force {
http.Redirect(w, r, redirect.To, redirect.Status)
return
}
tx := &redirectResponse{
error: nil,
statusCode: http.StatusOK,
ResponseWriter: w,
}
next(tx, r)
// NOTE(toby3d): redirect only if something bad on requested
// URL.
if tx.error == nil && http.StatusOK < tx.statusCode && tx.statusCode < http.StatusBadRequest {
return
}
http.Redirect(w, r, redirect.To, redirect.Status)
} }
} }
func (r *redirectResponse) WriteHeader(status int) {
r.statusCode = status
r.ResponseWriter.WriteHeader(status)
}
func (r *redirectResponse) Write(src []byte) (int, error) {
var length int
length, r.error = r.ResponseWriter.Write(src)
return length, r.error
}