From 6a3e89743ccad58097a6dd203a63448946a2304d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Erik=20Pedersen?= Date: Wed, 27 May 2020 13:50:13 +0200 Subject: Add redirect support to the server Fixes #7323 --- config/commonConfig.go | 81 +++++++++++++++++++++++++++++++++++++++------ config/commonConfig_test.go | 62 ++++++++++++++++++++++++++++++++-- 2 files changed, 131 insertions(+), 12 deletions(-) (limited to 'config') diff --git a/config/commonConfig.go b/config/commonConfig.go index ba99260a5..6444c03ad 100644 --- a/config/commonConfig.go +++ b/config/commonConfig.go @@ -14,6 +14,8 @@ package config import ( + "github.com/pkg/errors" + "sort" "strings" "sync" @@ -101,26 +103,36 @@ func DecodeSitemap(prototype Sitemap, input map[string]interface{}) Sitemap { // Config for the dev server. type Server struct { - Headers []Headers + Headers []Headers + Redirects []Redirect - compiledInit sync.Once - compiled []glob.Glob + compiledInit sync.Once + compiledHeaders []glob.Glob + compiledRedirects []glob.Glob } -func (s *Server) Match(pattern string) []types.KeyValueStr { +func (s *Server) init() { + s.compiledInit.Do(func() { for _, h := range s.Headers { - s.compiled = append(s.compiled, glob.MustCompile(h.For)) + s.compiledHeaders = append(s.compiledHeaders, glob.MustCompile(h.For)) + } + for _, r := range s.Redirects { + s.compiledRedirects = append(s.compiledRedirects, glob.MustCompile(r.From)) } }) +} - if s.compiled == nil { +func (s *Server) MatchHeaders(pattern string) []types.KeyValueStr { + s.init() + + if s.compiledHeaders == nil { return nil } var matches []types.KeyValueStr - for i, g := range s.compiled { + for i, g := range s.compiledHeaders { if g.Match(pattern) { h := s.Headers[i] for k, v := range h.Values { @@ -137,18 +149,67 @@ func (s *Server) Match(pattern string) []types.KeyValueStr { } +func (s *Server) MatchRedirect(pattern string) Redirect { + s.init() + + if s.compiledRedirects == nil { + return Redirect{} + } + + pattern = strings.TrimSuffix(pattern, "index.html") + + for i, g := range s.compiledRedirects { + redir := s.Redirects[i] + + // No redirect to self. + if redir.To == pattern { + return Redirect{} + } + + if g.Match(pattern) { + return redir + } + } + + return Redirect{} + +} + type Headers struct { For string Values map[string]interface{} } -func DecodeServer(cfg Provider) *Server { +type Redirect struct { + From string + To string + Status int +} + +func (r Redirect) IsZero() bool { + return r.From == "" +} + +func DecodeServer(cfg Provider) (*Server, error) { m := cfg.GetStringMap("server") s := &Server{} if m == nil { - return s + return s, nil } _ = mapstructure.WeakDecode(m, s) - return s + + for i, redir := range s.Redirects { + // Get it in line with the Hugo server. + redir.To = strings.TrimSuffix(redir.To, "index.html") + if !strings.HasPrefix(redir.To, "https") && !strings.HasSuffix(redir.To, "/") { + // There are some tricky infinite loop situations when dealing + // when the target does not have a trailing slash. + // This can certainly be handled better, but not time for that now. + return nil, errors.Errorf("unspported redirect to value %q in server config; currently this must be either a remote destination or a local folder, e.g. \"/blog/\" or \"/blog/index.html\"", redir.To) + } + s.Redirects[i] = redir + } + + return s, nil } diff --git a/config/commonConfig_test.go b/config/commonConfig_test.go index 41b2721bc..b8b6e6795 100644 --- a/config/commonConfig_test.go +++ b/config/commonConfig_test.go @@ -70,15 +70,73 @@ for = "/*.jpg" X-Frame-Options = "DENY" X-XSS-Protection = "1; mode=block" X-Content-Type-Options = "nosniff" + +[[server.redirects]] +from = "/foo/**" +to = "/foo/index.html" +status = 200 + +[[server.redirects]] +from = "/google/**" +to = "https://google.com/" +status = 301 + +[[server.redirects]] +from = "/**" +to = "/default/index.html" +status = 301 + + + `, "toml") c.Assert(err, qt.IsNil) - s := DecodeServer(cfg) + s, err := DecodeServer(cfg) + c.Assert(err, qt.IsNil) - c.Assert(s.Match("/foo.jpg"), qt.DeepEquals, []types.KeyValueStr{ + c.Assert(s.MatchHeaders("/foo.jpg"), qt.DeepEquals, []types.KeyValueStr{ {Key: "X-Content-Type-Options", Value: "nosniff"}, {Key: "X-Frame-Options", Value: "DENY"}, {Key: "X-XSS-Protection", Value: "1; mode=block"}}) + c.Assert(s.MatchRedirect("/foo/bar/baz"), qt.DeepEquals, Redirect{ + From: "/foo/**", + To: "/foo/", + Status: 200, + }) + + c.Assert(s.MatchRedirect("/someother"), qt.DeepEquals, Redirect{ + From: "/**", + To: "/default/", + Status: 301, + }) + + c.Assert(s.MatchRedirect("/google/foo"), qt.DeepEquals, Redirect{ + From: "/google/**", + To: "https://google.com/", + Status: 301, + }) + + // No redirect loop, please. + c.Assert(s.MatchRedirect("/default/index.html"), qt.DeepEquals, Redirect{}) + c.Assert(s.MatchRedirect("/default/"), qt.DeepEquals, Redirect{}) + + for _, errorCase := range []string{`[[server.redirects]] +from = "/**" +to = "/file" +status = 301`, + `[[server.redirects]] +from = "/**" +to = "/foo/file.html" +status = 301`, + } { + + cfg, err := FromConfigString(errorCase, "toml") + c.Assert(err, qt.IsNil) + _, err = DecodeServer(cfg) + c.Assert(err, qt.Not(qt.IsNil)) + + } + } -- cgit v1.2.3