~elektito/hodhod

2840dbb863345e820ab016bd3868af82317bd679 — Mostafa Razavi 1 year, 2 months ago 9df4cf1
Redirect to canonical URLs

URLs referring to a directory should always end in a slash. URLs
referring to a file should never end in a slash. We now redirect to the
correct form.

The reason this is important is that incorrect URLs can result to broken
relative links. For example, if the page at gemini://example.org/gemlog/
has a relative link to post.gmi, and we drop the trailing slash (so it
ends with "gemlog", instead of "gemlog/"), the link to post.gmi would
refer to "gemini://example.org/post.gmi" instead of
"gemini://example.org/gemlog/post.gmi".
3 files changed, 68 insertions(+), 2 deletions(-)

M main.go
A pkg/hodhod/redirect.go
M pkg/hodhod/static.go
M main.go => main.go +1 -1
@@ 90,7 90,7 @@ func getResponseForRequest(req hodhod.Request, cfg *hodhod.Config) (resp hodhod.

	if backend.Type == "static" {
		filename := path.Join(backend.Location, unmatched)
		resp = hodhod.NewFileResp(filename, cfg)
		resp = hodhod.NewFileResp(filename, req, cfg)
		return
	}


A pkg/hodhod/redirect.go => pkg/hodhod/redirect.go +55 -0
@@ 0,0 1,55 @@
package hodhod

import (
	"fmt"
	"io"
)

type RedirectResponse struct {
	StatusCode         int
	Target             string
	returnedStatusLine bool
}

func (resp *RedirectResponse) Backend() string {
	return "redirect"
}

func (resp *RedirectResponse) Init(req *Request) (err error) {
	return
}

func (resp *RedirectResponse) Read(p []byte) (n int, err error) {
	if resp.returnedStatusLine {
		return 0, io.EOF
	}

	status := []byte(fmt.Sprintf("%d %s\r\n", resp.StatusCode, resp.Target))
	if len(p) < len(status) {
		return 0, fmt.Errorf("Not enough space in read buffer")
	}
	copy(p, status)

	n = len(status)
	resp.returnedStatusLine = true
	return
}

func (resp *RedirectResponse) Close() {
}

func NewTempRedirectResp(target string) (resp Response) {
	return &RedirectResponse{
		StatusCode: 30,
		Target:     target,
	}
}

func NewPermRedirectResp(target string) (resp Response) {
	return &RedirectResponse{
		StatusCode: 31,
		Target:     target,
	}
}

var _ Response = (*RedirectResponse)(nil)

M pkg/hodhod/static.go => pkg/hodhod/static.go +12 -1
@@ 42,12 42,14 @@ func (resp *StaticResponse) Close() {
	}
}

func NewFileResp(filename string, cfg *Config) (resp Response) {
func NewFileResp(filename string, req Request, cfg *Config) (resp Response) {
	isDir := false
	f, err := os.Open(filename)

	if err == nil {
		info, serr := f.Stat()
		if serr == nil && info.IsDir() {
			isDir = true
			filename = path.Join(filename, cfg.MatchOptions.IndexFilename)
			f, err = os.Open(filename)
		}


@@ 71,6 73,15 @@ func NewFileResp(filename string, cfg *Config) (resp Response) {
		return
	}

	u := *req.Url
	if isDir && u.Path[len(u.Path)-1] != '/' {
		u.Path = u.Path + "/"
		return NewPermRedirectResp(u.String())
	} else if !isDir && u.Path[len(u.Path)-1] == '/' {
		u.Path = u.Path[:len(u.Path)-1]
		return NewPermRedirectResp(u.String())
	}

	ext := filepath.Ext(filename)
	if ext != "" {
		// remove leading dot