From f9ab36076a5a7474244664f879a214f5c241c510 Mon Sep 17 00:00:00 2001 From: Maxim Lebedev Date: Mon, 13 Nov 2023 09:02:46 +0600 Subject: [PATCH 1/4] :technologist: Created middleware package --- internal/middleware/middleware.go | 44 +++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 internal/middleware/middleware.go diff --git a/internal/middleware/middleware.go b/internal/middleware/middleware.go new file mode 100644 index 0000000..044c1ba --- /dev/null +++ b/internal/middleware/middleware.go @@ -0,0 +1,44 @@ +package middleware + +import "net/http" + +type ( + // Interceptor intercepts an HTTP handler invocation, it is passed both + // response writer and request which after interception can be passed + // onto the handler function. + Interceptor func(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) + + // HandlerFunc builds on top of http.HandlerFunc, and exposes API to + // intercept with Interceptor. This allows building complex long chains + // without complicated struct manipulation. + HandlerFunc http.HandlerFunc + + // Chain is a collection of interceptors that will be invoked in there + // index order. + Chain []Interceptor + + // Skipper is a requests checker for middleware configurations. true + // return force middleware to skip any actions with current request. + Skipper func(r *http.Request) bool +) + +var DefaultSkipper Skipper = func(_ *http.Request) bool { return false } + +// Intercept returns back a continuation that will call install middleware to +// intercept the continuation call. +func (hf HandlerFunc) Intercept(i Interceptor) HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + i(w, r, http.HandlerFunc(hf)) + } +} + +// Handler allows hooking multiple middleware in single call. +func (c Chain) Handler(hf http.HandlerFunc) http.Handler { + current := HandlerFunc(hf) + + for i := len(c) - 1; i >= 0; i-- { + current = current.Intercept(c[i]) + } + + return http.HandlerFunc(current) +} From 7f294a1f80833fa931d3aee0a279241f763f1560 Mon Sep 17 00:00:00 2001 From: Maxim Lebedev Date: Mon, 13 Nov 2023 09:12:36 +0600 Subject: [PATCH 2/4] :technologist: Created basic redirect middleware --- internal/middleware/redirect.go | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 internal/middleware/redirect.go diff --git a/internal/middleware/redirect.go b/internal/middleware/redirect.go new file mode 100644 index 0000000..1b1a2f6 --- /dev/null +++ b/internal/middleware/redirect.go @@ -0,0 +1,32 @@ +package middleware + +import ( + "net/http" + "net/url" +) + +type RedirectConfig struct { + Skipper Skipper + URL *url.URL + Code int +} + +func Redirect(config RedirectConfig) Interceptor { + if config.Skipper == nil { + config.Skipper = DefaultSkipper + } + + if config.Code == 0 { + config.Code = http.StatusMovedPermanently + } + + return func(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) { + if config.Skipper(r) { + next(w, r) + + return + } + + http.RedirectHandler(config.URL.String(), config.Code).ServeHTTP(w, r) + } +} From aa7cba5e143bcb71a01f38d8047f997dce318357 Mon Sep 17 00:00:00 2001 From: Maxim Lebedev Date: Mon, 13 Nov 2023 09:37:01 +0600 Subject: [PATCH 3/4] :recycle: Improved redirecting middleware configuration --- internal/middleware/redirect.go | 31 ++++++++++++++++++++++++------- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/internal/middleware/redirect.go b/internal/middleware/redirect.go index 1b1a2f6..5db5791 100644 --- a/internal/middleware/redirect.go +++ b/internal/middleware/redirect.go @@ -5,13 +5,16 @@ import ( "net/url" ) -type RedirectConfig struct { - Skipper Skipper - URL *url.URL - Code int -} +type ( + RedirectConfig struct { + Skipper Skipper + Code int + } -func Redirect(config RedirectConfig) Interceptor { + redirectLogic func(u *url.URL) (url string, ok bool) +) + +func Redirect(config RedirectConfig, redirect redirectLogic) Interceptor { if config.Skipper == nil { config.Skipper = DefaultSkipper } @@ -27,6 +30,20 @@ func Redirect(config RedirectConfig) Interceptor { return } - http.RedirectHandler(config.URL.String(), config.Code).ServeHTTP(w, r) + u := &url.URL{ + Scheme: "http", + Host: r.Host, + Path: r.RequestURI, + } + + if r.TLS != nil { + u.Scheme += "s" + } + + if target, ok := redirect(u); ok { + http.RedirectHandler(target, config.Code).ServeHTTP(w, r) + } else { + next(w, r) + } } } From 0558cf72d284955091d7e5828c7cf9a5dfe42e8f Mon Sep 17 00:00:00 2001 From: Maxim Lebedev Date: Mon, 13 Nov 2023 10:29:25 +0600 Subject: [PATCH 4/4] :construction: Trying to setup redirect middleware for Telegram IV --- main.go | 203 +++++++++++++++++++++++++++++++++----------------------- 1 file changed, 120 insertions(+), 83 deletions(-) diff --git a/main.go b/main.go index e83ef30..51ed911 100644 --- a/main.go +++ b/main.go @@ -13,8 +13,10 @@ import ( "log" "net" "net/http" + "net/url" "os" "os/signal" + "path" "path/filepath" "runtime" "runtime/pprof" @@ -28,6 +30,7 @@ import ( "source.toby3d.me/toby3d/home/internal/common" "source.toby3d.me/toby3d/home/internal/domain" + "source.toby3d.me/toby3d/home/internal/middleware" "source.toby3d.me/toby3d/home/internal/page" pagefsrepo "source.toby3d.me/toby3d/home/internal/page/repository/fs" pageucase "source.toby3d.me/toby3d/home/internal/page/usecase" @@ -80,117 +83,151 @@ func NewApp(ctx context.Context, config *domain.Config) (*App, error) { pages := pagefsrepo.NewFileSystemPageRepository(contentDir) pager := pageucase.NewPageUseCase(pages, resources) - server := &http.Server{ - Addr: config.AddrPort().String(), - Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // TODO(toby3d): use exists static use case or split that on static and resource modules? - // 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 - // something else. - static, err := statics.Get(r.Context(), strings.TrimPrefix(r.URL.Path, "/")) - if err == nil { - http.ServeContent(w, r, static.Name(), domain.ResourceModTime(static), static) + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // TODO(toby3d): use exists static use case or split that on static and resource modules? + // 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 something else. + static, err := statics.Get(r.Context(), strings.TrimPrefix(r.URL.Path, "/")) + if err == nil { + http.ServeContent(w, r, static.Name(), domain.ResourceModTime(static), static) + + return + } + + lang := domain.LanguageUnd + + s, err := siter.Do(r.Context(), lang) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + + return + } + + if s.IsMultiLingual() { + head, tail := urlutil.ShiftPath(r.URL.Path) + + if head == "" { + supported := make([]language.Tag, len(s.Languages)) + for i := range s.Languages { + supported[i] = language.Make(s.Languages[i].Lang()) + } + + if s.DefaultLanguage != domain.LanguageUnd { + supported = append( + []language.Tag{language.Make(s.DefaultLanguage.Code())}, + supported..., + ) + } + + requested, _, err := language.ParseAcceptLanguage( + r.Header.Get(common.HeaderAcceptLanguage)) + if err != nil || len(requested) == 0 { + requested = append(requested, language.English) + } + + matched, _, _ := language.NewMatcher(supported).Match(requested...) + lang = domain.NewLanguage(matched.String()) + + http.Redirect(w, r, "/"+lang.Lang()+"/", http.StatusSeeOther) return } - lang := domain.LanguageUnd + lang = domain.NewLanguage(head) + r.URL.Path = tail + } - s, err := siter.Do(r.Context(), lang) - if err != nil { + if s, err = siter.Do(r.Context(), lang); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + + return + } + + p, err := pager.Do(r.Context(), lang, r.URL.Path) + if err != nil { + if !errors.Is(err, page.ErrNotExist) { http.Error(w, err.Error(), http.StatusInternalServerError) return } - if s.IsMultiLingual() { - head, tail := urlutil.ShiftPath(r.URL.Path) - - if head == "" { - supported := make([]language.Tag, len(s.Languages)) - for i := range s.Languages { - supported[i] = language.Make(s.Languages[i].Lang()) - } - - if s.DefaultLanguage != domain.LanguageUnd { - supported = append( - []language.Tag{language.Make(s.DefaultLanguage.Code())}, - supported..., - ) - } - - requested, _, err := language.ParseAcceptLanguage( - r.Header.Get(common.HeaderAcceptLanguage)) - if err != nil || len(requested) == 0 { - requested = append(requested, language.English) - } - - matched, _, _ := language.NewMatcher(supported).Match(requested...) - lang = domain.NewLanguage(matched.String()) - - http.Redirect(w, r, "/"+lang.Lang()+"/", http.StatusSeeOther) + res, err := resourcer.Do(r.Context(), r.URL.Path) + if err != nil { + if errors.Is(err, fs.ErrNotExist) { + http.Error(w, err.Error(), http.StatusNotFound) return } - lang = domain.NewLanguage(head) - r.URL.Path = tail - } - - if s, err = siter.Do(r.Context(), lang); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } - p, err := pager.Do(r.Context(), lang, r.URL.Path) - if err != nil { - if !errors.Is(err, page.ErrNotExist) { - http.Error(w, err.Error(), http.StatusInternalServerError) + http.ServeContent(w, r, res.Name(), domain.ResourceModTime(res), res) - return + return + } + + contentLanguage := make([]string, len(p.Translations)) + for i := range p.Translations { + contentLanguage[i] = p.Translations[i].Language.Code() + } + + w.Header().Set(common.HeaderContentLanguage, strings.Join(contentLanguage, ", ")) + + tpl, err := themer.Do(r.Context()) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + + return + } + + w.Header().Set(common.HeaderContentType, common.MIMETextHTMLCharsetUTF8) + if err = tpl.Execute(w, &Context{ + Site: s, + Page: p, + }); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + }) + + mw := middleware.Chain{ + middleware.Redirect(middleware.RedirectConfig{ + Code: http.StatusSeeOther, + Skipper: func(r *http.Request) bool { + return r.UserAgent() != "TelegramBot (like TwitterBot)" + }, + }, func(u *url.URL) (string, bool) { + s, err := siter.Do(ctx, domain.LanguageUnd) + if err != nil { + return "", false + } + + if ivHash, ok := s.Params["instantViewHash"].(string); ok { + target := &url.URL{ + Scheme: "https", + Host: "t.me", + Path: path.Join("/share", "url"), } - res, err := resourcer.Do(r.Context(), r.URL.Path) - if err != nil { - if errors.Is(err, fs.ErrNotExist) { - http.Error(w, err.Error(), http.StatusNotFound) + q := target.Query() - return - } + q.Set("url", u.String()) + q.Set("hash", ivHash) - http.Error(w, err.Error(), http.StatusInternalServerError) + target.RawQuery = q.Encode() - return - } - - http.ServeContent(w, r, res.Name(), domain.ResourceModTime(res), res) - - return + return target.String(), ok } - contentLanguage := make([]string, len(p.Translations)) - for i := range p.Translations { - contentLanguage[i] = p.Translations[i].Language.Code() - } - - w.Header().Set(common.HeaderContentLanguage, strings.Join(contentLanguage, ", ")) - - tpl, err := themer.Do(r.Context()) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - - return - } - - w.Header().Set(common.HeaderContentType, common.MIMETextHTMLCharsetUTF8) - if err = tpl.Execute(w, &Context{ - Site: s, - Page: p, - }); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - } + return "", false }), + } + + server := &http.Server{ + Addr: config.AddrPort().String(), + Handler: mw.Handler(handler), ErrorLog: logger, BaseContext: func(ln net.Listener) context.Context { return ctx }, WriteTimeout: 500 * time.Millisecond,