♻️ 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)
pages := pagefsrepo.NewFileSystemPageRepository(contentDir)
pager := pageucase.NewPageUseCase(pages, resources)
server := servercase.NewServerUseCase()
serverer := servercase.NewServerUseCase()
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
// 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
}
siteServer, err := server.Do(r.Context(), *s)
siteServer, err := serverer.Do(r.Context(), *s)
if err != nil {
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() {
head, tail := urlutil.ShiftPath(r.URL.Path)
if head == "" {
supported := make([]language.Tag, len(s.Languages))
for i := range s.Languages {
@ -125,8 +107,8 @@ func NewApp(logger *log.Logger, config *domain.Config) (*App, error) {
supported...)
}
requested, _, err := language.ParseAcceptLanguage(
r.Header.Get(common.HeaderAcceptLanguage))
requested, _, err := language.ParseAcceptLanguage(r.Header.Get(
common.HeaderAcceptLanguage))
if err != nil || len(requested) == 0 {
requested = append(requested, language.English)
}
@ -139,14 +121,9 @@ func NewApp(logger *log.Logger, config *domain.Config) (*App, error) {
return
}
lang = domain.NewLanguage(head)
r.URL.Path = tail
}
if lang == domain.LanguageUnd && redirect != nil {
http.Redirect(w, r, redirect.To, redirect.Status)
return
if lang = domain.NewLanguage(head); lang != domain.LanguageUnd {
r.URL.Path = tail
}
}
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
}
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)
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)
if err != nil {
if !errors.Is(err, page.ErrNotExist) {
@ -195,12 +156,6 @@ func NewApp(logger *log.Logger, config *domain.Config) (*App, error) {
return
}
if redirect != nil {
http.Redirect(w, r, redirect.To, redirect.Status)
return
}
res, err := resourcer.Do(r.Context(), r.URL.Path)
if err != nil {
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)
}
})
chain := middleware.Chain{middleware.LogFmt()}
chain := middleware.Chain{
middleware.LogFmt(),
middleware.Redirect(middleware.RedirectConfig{
Siter: siter,
Serverer: serverer,
}),
}
return &App{server: &http.Server{
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 {
Headers []Header
Redirects []Redirect
Redirects Redirects
}
func NewServer() *Server {
return &Server{
Headers: make([]Header, 0),
Redirects: make([]Redirect, 0),
Redirects: make(Redirects, 0),
}
}

View File

@ -2,25 +2,38 @@ package middleware
import (
"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 (
RedirectConfig struct {
Skipper Skipper
Code int
Skipper Skipper
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 {
config.Skipper = DefaultSkipper
}
if config.Code == 0 {
config.Code = http.StatusMovedPermanently
if config.Siter == nil {
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) {
@ -30,20 +43,69 @@ func Redirect(config RedirectConfig, redirect redirectLogic) Interceptor {
return
}
u := &url.URL{
Scheme: "http",
Host: r.Host,
Path: r.RequestURI,
lang, path := domain.LanguageUnd, r.URL.Path
if head, tail := urlutil.ShiftPath(r.URL.Path); head != "" {
if lang = domain.NewLanguage(head); lang != domain.LanguageUnd {
path = tail
}
}
if r.TLS != nil {
u.Scheme += "s"
site, err := config.Siter.Do(r.Context(), lang)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if target, ok := redirect(u); ok {
http.RedirectHandler(target, config.Code).ServeHTTP(w, r)
} else {
server, err := config.Serverer.Do(r.Context(), *site)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
redirect, ok := server.Redirects.Match(path)
if !ok {
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
}