Compare commits

...

18 Commits

Author SHA1 Message Date
Maxim Lebedev 5adf0b6bef
👔 Set default status code for redirect if not specified
/ docker (push) Successful in 1m19s Details
2023-12-09 13:42:50 +06:00
Maxim Lebedev f32551a748
🐛 Fixed panic in language specific redirect 2023-12-09 13:42:13 +06:00
Maxim Lebedev 8882fb12e9
Merge branch 'feature/server' into develop 2023-12-09 13:13:57 +06:00
Maxim Lebedev 4fbf3e3659
🏗️ Used server module in home app for redirects 2023-12-09 13:13:47 +06:00
Maxim Lebedev 421a45afcb
👔 Added redirects support in server use case 2023-12-09 12:43:36 +06:00
Maxim Lebedev 8db047603d
🏷️ Added redirects params in TestSite output 2023-12-09 12:43:04 +06:00
Maxim Lebedev 739a38eb4c
🏷️ Added IsMatch method for redirect domain 2023-12-09 12:42:45 +06:00
Maxim Lebedev 0c2f6b560b
🏗️ Used server module in home app for headers edits 2023-12-09 12:29:57 +06:00
Maxim Lebedev 1b987f1f0d
Added one more header value for server use case test 2023-12-09 12:28:27 +06:00
Maxim Lebedev 50f6c7fabe
🎨 Fixed server params names in TestSite output 2023-12-09 12:26:50 +06:00
Maxim Lebedev f73500580a
🏷️ Added IsMatched method for Header domain 2023-12-09 12:25:53 +06:00
Maxim Lebedev 10be636a05
👔 Changed params names for server module data 2023-12-09 12:25:28 +06:00
Maxim Lebedev dac36d3e71
👔 Created server use case module implementation 2023-12-09 12:03:54 +06:00
Maxim Lebedev 7d5b04e642
👔 Created use case for server module 2023-12-09 12:03:35 +06:00
Maxim Lebedev c05feef09b
🧑‍💻 Added TestSite util for tests 2023-12-09 12:02:33 +06:00
Maxim Lebedev 5d3bd2e334
🏷️ Created Server domain 2023-12-09 12:02:08 +06:00
Maxim Lebedev 2f12e09d4c
🏷️ Created Redirect domain 2023-12-09 12:01:49 +06:00
Maxim Lebedev 83f1aa8880
🏷️ Created Header domain 2023-12-09 12:01:37 +06:00
8 changed files with 365 additions and 7 deletions

View File

@ -24,6 +24,7 @@ import (
pageucase "source.toby3d.me/toby3d/home/internal/page/usecase"
resourcefsrepo "source.toby3d.me/toby3d/home/internal/resource/repository/fs"
resourceucase "source.toby3d.me/toby3d/home/internal/resource/usecase"
servercase "source.toby3d.me/toby3d/home/internal/server/usecase"
sitefsrepo "source.toby3d.me/toby3d/home/internal/site/repository/fs"
siteucase "source.toby3d.me/toby3d/home/internal/site/usecase"
staticfsrepo "source.toby3d.me/toby3d/home/internal/static/repository/fs"
@ -56,6 +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()
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
@ -76,6 +78,40 @@ func NewApp(logger *log.Logger, config *domain.Config) (*App, error) {
return
}
siteServer, err := server.Do(r.Context(), *s)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
for i := range siteServer.Headers {
if !siteServer.Headers[i].IsMatched(r.URL.Path) {
continue
}
for name, value := range siteServer.Headers[i].Headers {
w.Header().Set(name, value)
}
}
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)
@ -86,9 +122,8 @@ func NewApp(logger *log.Logger, config *domain.Config) (*App, error) {
}
if s.DefaultLanguage != domain.LanguageUnd {
supported = append(
[]language.Tag{language.Make(s.DefaultLanguage.Code())}, supported...,
)
supported = append([]language.Tag{language.Make(s.DefaultLanguage.Code())},
supported...)
}
requested, _, err := language.ParseAcceptLanguage(
@ -109,12 +144,50 @@ func NewApp(logger *log.Logger, config *domain.Config) (*App, error) {
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 {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if siteServer, err = server.Do(r.Context(), *s); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
for i := range siteServer.Headers {
if !siteServer.Headers[i].IsMatched(r.URL.Path) {
continue
}
for name, value := range siteServer.Headers[i].Headers {
w.Header().Set(name, value)
}
}
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) {
@ -123,6 +196,12 @@ 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) {
@ -178,14 +257,13 @@ func NewApp(logger *log.Logger, config *domain.Config) (*App, error) {
}
})
chain := middleware.Chain{middleware.LogFmt()}
server := &http.Server{
return &App{server: &http.Server{
Addr: config.AddrPort().String(),
Handler: chain.Handler(handler),
ErrorLog: logger,
WriteTimeout: 500 * time.Millisecond,
}
return &App{server: server}, nil
}}, nil
}
func (a *App) Run(ln net.Listener) error {

19
internal/domain/header.go Normal file
View File

@ -0,0 +1,19 @@
package domain
import (
"path"
)
type Header struct {
Headers map[string]string
Path string
}
func (h Header) IsMatched(p string) bool {
matched, err := path.Match(h.Path, p)
if err != nil {
return false
}
return matched
}

View File

@ -0,0 +1,19 @@
package domain
import "path"
type Redirect struct {
From string
To string
Status int
Force bool
}
func (r Redirect) IsMatch(p string) bool {
matched, err := path.Match(r.From, p)
if err != nil {
return false
}
return matched
}

13
internal/domain/server.go Normal file
View File

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

View File

@ -3,6 +3,8 @@ package domain
import (
"net/url"
"path"
"path/filepath"
"testing"
"time"
)
@ -29,3 +31,48 @@ func (s Site) LanguagePrefix() string {
func (s Site) IsMultiLingual() bool {
return 1 < len(s.Languages)
}
func TestSite(tb testing.TB) *Site {
tb.Helper()
ru := NewLanguage("ru")
en := NewLanguage("en")
return &Site{
DefaultLanguage: en,
Language: ru,
Languages: []Language{en, ru},
BaseURL: &url.URL{Scheme: "http", Host: "127.0.0.1:3000", Path: "/"},
TimeZone: time.UTC,
File: NewFile(filepath.Join("content", "index.en.md")),
Title: "Testing",
Resources: make([]*Resource, 0),
Params: map[string]any{
"server": map[string]any{
"headers": []any{map[string]any{
"path": "/**",
"values": map[string]any{
"Link": `<https://auth.example.com/>; rel="indieauth-metadata", ` +
`<https://pub.example.com/>; rel="micropub"`,
"X-Koroko": "Ya-ha-ha!",
},
}, map[string]any{
"path": "/foo/bar",
"values": map[string]any{
"X-Testing": `sample-text`,
},
}},
"redirects": []any{map[string]any{
"from": "/foo/bar",
"to": "/bar/foo",
"status": 302,
}, map[string]any{
"from": "/foo",
"to": "https://example.com/",
"status": 301,
"force": true,
}},
},
},
}
}

View File

@ -0,0 +1,19 @@
package server
import (
"context"
"errors"
"source.toby3d.me/toby3d/home/internal/domain"
)
type UseCase interface {
Do(ctx context.Context, site domain.Site) (*domain.Server, error)
}
var (
ErrParams error = errors.New("site not contains any params")
ErrServer error = errors.New("site not contains 'server' param")
ErrServerHeaders error = errors.New("'server' param in site not contains 'headers' param")
ErrServerRedirects error = errors.New("'server' param in site not contains 'redirects' param")
)

View File

@ -0,0 +1,111 @@
package usecase
import (
"context"
"net/http"
"source.toby3d.me/toby3d/home/internal/domain"
"source.toby3d.me/toby3d/home/internal/server"
)
type serverUseCase struct{}
func NewServerUseCase() server.UseCase {
return serverUseCase{}
}
func (serverUseCase) Do(ctx context.Context, site domain.Site) (*domain.Server, error) {
if site.Params == nil {
return nil, server.ErrParams
}
serverMap, ok := site.Params["server"].(map[string]any)
if !ok {
return nil, server.ErrServer
}
out := domain.NewServer()
parseHeaders(out, serverMap)
parseRedirects(out, serverMap)
return out, nil
}
func parseHeaders(dst *domain.Server, params map[string]any) error {
serverHeadersValues, ok := params["headers"].([]any)
if !ok {
return server.ErrServerHeaders
}
for i := range serverHeadersValues {
headerMap, ok := serverHeadersValues[i].(map[string]any)
if !ok {
continue
}
header := domain.Header{
Headers: make(map[string]string),
Path: "",
}
if header.Path, ok = headerMap["path"].(string); !ok {
continue
}
values, ok := headerMap["values"].(map[string]any)
if !ok {
continue
}
for name, value := range values {
v, ok := value.(string)
if !ok {
continue
}
header.Headers[name] = v
}
dst.Headers = append(dst.Headers, header)
}
return nil
}
func parseRedirects(dst *domain.Server, params map[string]any) error {
serverRedirectsValues, ok := params["redirects"].([]any)
if !ok {
return server.ErrServerRedirects
}
for i := range serverRedirectsValues {
redirectMap, ok := serverRedirectsValues[i].(map[string]any)
if !ok {
continue
}
redirect := domain.Redirect{
From: "",
To: "",
Status: http.StatusMovedPermanently,
Force: false,
}
if redirect.From, ok = redirectMap["from"].(string); !ok {
continue
}
if redirect.To, ok = redirectMap["to"].(string); !ok {
continue
}
if status, ok := redirectMap["status"].(int); ok && status != 0 {
redirect.Status = status
}
redirect.Force, _ = redirectMap["force"].(bool)
dst.Redirects = append(dst.Redirects, redirect)
}
return nil
}

View File

@ -0,0 +1,52 @@
package usecase_test
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"source.toby3d.me/toby3d/home/internal/domain"
"source.toby3d.me/toby3d/home/internal/server/usecase"
)
func TestDo(t *testing.T) {
t.Parallel()
site := domain.TestSite(t)
actual, err := usecase.NewServerUseCase().Do(context.Background(), *site)
if err != nil {
t.Fatal(err)
}
expect := &domain.Server{
Headers: []domain.Header{{
Path: "/**",
Headers: map[string]string{
"Link": `<https://auth.example.com/>; rel="indieauth-metadata", ` +
`<https://pub.example.com/>; rel="micropub"`,
"X-Koroko": "Ya-ha-ha!",
},
}, {
Path: "/foo/bar",
Headers: map[string]string{
"X-Testing": `sample-text`,
},
}},
Redirects: []domain.Redirect{{
From: "/foo/bar",
To: "/bar/foo",
Status: 302,
Force: false,
}, {
From: "/foo",
To: "https://example.com/",
Status: 301,
Force: true,
}},
}
if diff := cmp.Diff(actual, expect); diff != "" {
t.Error(diff)
}
}