Compare commits
18 Commits
8a8372bdf0
...
5adf0b6bef
Author | SHA1 | Date |
---|---|---|
Maxim Lebedev | 5adf0b6bef | |
Maxim Lebedev | f32551a748 | |
Maxim Lebedev | 8882fb12e9 | |
Maxim Lebedev | 4fbf3e3659 | |
Maxim Lebedev | 421a45afcb | |
Maxim Lebedev | 8db047603d | |
Maxim Lebedev | 739a38eb4c | |
Maxim Lebedev | 0c2f6b560b | |
Maxim Lebedev | 1b987f1f0d | |
Maxim Lebedev | 50f6c7fabe | |
Maxim Lebedev | f73500580a | |
Maxim Lebedev | 10be636a05 | |
Maxim Lebedev | dac36d3e71 | |
Maxim Lebedev | 7d5b04e642 | |
Maxim Lebedev | c05feef09b | |
Maxim Lebedev | 5d3bd2e334 | |
Maxim Lebedev | 2f12e09d4c | |
Maxim Lebedev | 83f1aa8880 |
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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),
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
}},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
)
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue