Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add redirect support to the server #7327

Merged
merged 1 commit into from
May 28, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion commands/commandeer.go
Original file line number Diff line number Diff line change
Expand Up @@ -346,7 +346,10 @@ func (c *commandeer) loadConfig(mustHaveConfigFile, running bool) error {

cfg.Logger = logger
c.logger = logger
c.serverConfig = hconfig.DecodeServer(cfg.Cfg)
c.serverConfig, err = hconfig.DecodeServer(cfg.Cfg)
if err != nil {
return err
}

createMemFs := config.GetBool("renderToMemory")

Expand Down
30 changes: 29 additions & 1 deletion commands/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,18 @@ type fileServer struct {
s *serverCmd
}

func (f *fileServer) rewriteRequest(r *http.Request, toPath string) *http.Request {
r2 := new(http.Request)
*r2 = *r
r2.URL = new(url.URL)
*r2.URL = *r.URL
r2.URL.Path = toPath
r2.Header.Set("X-Rewrite-Original-URI", r.URL.RequestURI())

return r2

}

func (f *fileServer) createEndpoint(i int) (*http.ServeMux, string, string, error) {
baseURL := f.baseURLs[i]
root := f.roots[i]
Expand Down Expand Up @@ -356,10 +368,25 @@ func (f *fileServer) createEndpoint(i int) (*http.ServeMux, string, string, erro
w.Header().Set("Pragma", "no-cache")
}

for _, header := range f.c.serverConfig.Match(r.RequestURI) {
for _, header := range f.c.serverConfig.MatchHeaders(r.RequestURI) {
w.Header().Set(header.Key, header.Value)
}

if redirect := f.c.serverConfig.MatchRedirect(r.RequestURI); !redirect.IsZero() {
// This matches Netlify's behaviour and is needed for SPA behaviour.
// See https://docs.netlify.com/routing/redirects/rewrites-proxies/
if redirect.Status == 200 {
if r2 := f.rewriteRequest(r, strings.TrimPrefix(redirect.To, u.Path)); r2 != nil {
r = r2
}
} else {
w.Header().Set("Content-Type", "")
http.Redirect(w, r, redirect.To, redirect.Status)
return
}

}

if f.c.fastRenderMode && f.c.buildErr == nil {

p := strings.TrimSuffix(r.RequestURI, "?"+r.URL.RawQuery)
Expand All @@ -379,6 +406,7 @@ func (f *fileServer) createEndpoint(i int) (*http.ServeMux, string, string, erro

}
}

h.ServeHTTP(w, r)
})
}
Expand Down
81 changes: 71 additions & 10 deletions config/commonConfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
package config

import (
"github.com/pkg/errors"

"sort"
"strings"
"sync"
Expand Down Expand Up @@ -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 {
Expand All @@ -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
}
62 changes: 60 additions & 2 deletions config/commonConfig_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))

}

}
14 changes: 14 additions & 0 deletions docs/content/en/getting-started/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,20 @@ Content-Security-Policy = "script-src localhost:1313"
{{< /code-toggle >}}


{{< new-in "0.72.0" >}}

You can also specify simple redirects rules for the server. The syntax is again similar to Netlify's.

Note that a `status` code of 200 will trigger a [URL rewrite](https://docs.netlify.com/routing/redirects/rewrites-proxies/), which is what you want in SPA situations, e.g:

{{< code-toggle file="config/development/server">}}
[[redirects]]
from = "/myspa/**"
to = "/myspa/"
status = 200
{{< /code-toggle >}}




## Configure Title Case
Expand Down