aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorShulhan <ms@kilabit.info>2026-02-05 19:26:29 +0700
committerShulhan <ms@kilabit.info>2026-02-05 19:26:29 +0700
commit3734420eec485bca8c18243741e1ad2a683515b7 (patch)
treec08f208e76f516048e07e982fa712daafc98cce3
parentf40153cfb1cd859538aeaad17f0a243b4ef0c52c (diff)
downloadpakakeh.go-3734420eec485bca8c18243741e1ad2a683515b7.tar.xz
lib/http: handle file system with canonical directory end with slash "/"
Previously, if request to directory does not end with "/", the HTTP server will return the index.html of that directory. This cause relative link inside the index.html broken when visited from browser. This changes make the request to directory always end with "/" by redirecting the request with status 303 Found.
-rw-r--r--lib/http/server.go107
-rw-r--r--lib/http/server_test.go90
-rw-r--r--lib/http/testdata/Server_HandleFS/b.html6
-rw-r--r--lib/http/testdata/Server_HandleFS/dir/index.html6
-rw-r--r--lib/http/testdata/Server_HandleFS/index.html6
-rw-r--r--lib/http/testdata/Server_HandleFS_test.txt98
6 files changed, 216 insertions, 97 deletions
diff --git a/lib/http/server.go b/lib/http/server.go
index 76713308..edc99cf7 100644
--- a/lib/http/server.go
+++ b/lib/http/server.go
@@ -12,6 +12,7 @@ import (
"io"
"log"
"net/http"
+ "net/url"
"os"
"path"
"sort"
@@ -384,60 +385,67 @@ func (srv *Server) Stop(wait time.Duration) (err error) {
// getFSNode get the memfs Node based on the request path.
//
-// If the path is not exist, try path with index.html;
-// if it still not exist try path with suffix .html.
+// If the path is not exist, try path with ".html".
//
-// If the path is directory and contains index.html, the node for index.html
-// with true will be returned.
+// If the path is directory and contains index.html, the node content for
+// index.html will be returned.
//
// If the path is directory and does not contains index.html and
// [ServerOptions.EnableIndexHTML] is true, server will generate list of
// content for index.html.
-func (srv *Server) getFSNode(reqPath string) (node *memfs.Node) {
+//
+// The redirectPath return whether the server should redirect first before
+// serving the content to provide consistent canonical path that end with "/"
+// for directory that contains "index.html" or EnableIndexHTML is on.
+func (srv *Server) getFSNode(reqURL url.URL) (node *memfs.Node, redirectURL *url.URL) {
if srv.Options.Memfs == nil {
- return nil
+ return nil, nil
}
+ reqPath := reqURL.Path
+ endWithSlash := reqPath[len(reqPath)-1] == '/'
var err error
+ // The [MemFS.Get] method check reqPath with trailing `/` removed, so
+ // "path" or "path/" will return the same node.
node, err = srv.Options.Memfs.Get(reqPath)
if err != nil {
if !errors.Is(err, os.ErrNotExist) {
- return nil
+ return nil, nil
}
-
- var pathHTML = path.Join(reqPath, `index.html`)
-
- node, err = srv.Options.Memfs.Get(pathHTML)
- if err != nil {
- pathHTML = reqPath + `.html`
+ if !endWithSlash {
+ pathHTML := reqPath + `.html`
node, err = srv.Options.Memfs.Get(pathHTML)
if err != nil {
- return nil
+ return nil, nil
}
- return node
+ return node, nil
}
+ return nil, nil
+ }
+ if !node.IsDir() {
+ return node, nil
}
- if node.IsDir() {
- var (
- pathHTML = path.Join(reqPath, `index.html`)
- nodeIndexHTML *memfs.Node
- )
-
- nodeIndexHTML, err = srv.Options.Memfs.Get(pathHTML)
- if err == nil {
- return nodeIndexHTML
- }
-
- if !srv.Options.EnableIndexHTML {
- return node
+ pathIndexHTML := path.Join(reqPath, `index.html`)
+ nodeIndexHTML, err := srv.Options.Memfs.Get(pathIndexHTML)
+ if err != nil {
+ if srv.Options.EnableIndexHTML {
+ if !endWithSlash {
+ redirectURL = reqURL.JoinPath(`/`)
+ return nil, redirectURL
+ }
+ node.GenerateIndexHTML()
+ return node, nil
}
-
- node.GenerateIndexHTML()
+ return nil, nil
+ }
+ if !endWithSlash {
+ redirectURL = reqURL.JoinPath(`/`)
+ return nil, redirectURL
}
- return node
+ return nodeIndexHTML, nil
}
// handleDelete handle the DELETE request by searching the registered route
@@ -468,32 +476,14 @@ func (srv *Server) handleDelete(res http.ResponseWriter, req *http.Request) {
//
// If the request Path is not exist it will return 404 Not Found.
func (srv *Server) HandleFS(res http.ResponseWriter, req *http.Request) {
- var (
- logp = "HandleFS"
-
- node *memfs.Node
- err error
- )
-
- node = srv.getFSNode(req.URL.Path)
- if node == nil {
- if srv.Options.HandleFS == nil {
- res.WriteHeader(http.StatusNotFound)
- return
- }
- // Fallthrough, call HandleFS below.
- } else if node.IsDir() && req.URL.Path[len(req.URL.Path)-1] != '/' {
+ node, redirectURL := srv.getFSNode(*req.URL)
+ if redirectURL != nil {
// If request path is a directory and it is not end with
// slash, redirect request to location with slash to allow
// relative links works inside the HTML content.
- var redirectPath = req.URL.Path + "/"
- if len(req.URL.RawQuery) > 0 {
- redirectPath += "?" + req.URL.RawQuery
- }
- http.Redirect(res, req, redirectPath, http.StatusFound)
+ http.Redirect(res, req, redirectURL.String(), http.StatusFound)
return
}
-
if srv.Options.HandleFS != nil {
var statusCode int
node, statusCode = srv.Options.HandleFS(node, res, req)
@@ -522,9 +512,7 @@ func (srv *Server) HandleFS(res http.ResponseWriter, req *http.Request) {
var ifModifiedSince = req.Header.Get(HeaderIfModifiedSince)
if len(ifModifiedSince) != 0 {
- var timeModsince time.Time
-
- timeModsince, err = time.Parse(time.RFC1123, ifModifiedSince)
+ timeModsince, err := time.Parse(time.RFC1123, ifModifiedSince)
if err == nil {
if nodeModtime <= timeModsince.Unix() {
res.WriteHeader(http.StatusNotModified)
@@ -542,8 +530,7 @@ func (srv *Server) HandleFS(res http.ResponseWriter, req *http.Request) {
bodyReader = bytes.NewReader(node.Content)
size = node.Size()
} else {
- var f *os.File
- f, err = os.Open(node.SysPath)
+ f, err := os.Open(node.SysPath)
if err != nil {
res.WriteHeader(http.StatusInternalServerError)
return
@@ -577,7 +564,7 @@ func (srv *Server) HandleFS(res http.ResponseWriter, req *http.Request) {
return
}
- responseWrite(logp, res, req, bodyReader)
+ responseWrite(`HandleFS`, res, req, bodyReader)
}
// handleGet handle the GET request by searching the registered route and
@@ -653,8 +640,8 @@ func (srv *Server) handleHead(res http.ResponseWriter, req *http.Request) {
func (srv *Server) handleOptions(res http.ResponseWriter, req *http.Request) {
methods := make(map[string]bool)
- var node = srv.getFSNode(req.URL.Path)
- if node != nil {
+ node, redirectURL := srv.getFSNode(*req.URL)
+ if node != nil || redirectURL != nil {
methods[http.MethodGet] = true
methods[http.MethodHead] = true
}
diff --git a/lib/http/server_test.go b/lib/http/server_test.go
index 6542d833..a333628c 100644
--- a/lib/http/server_test.go
+++ b/lib/http/server_test.go
@@ -1,6 +1,5 @@
-// SPDX-FileCopyrightText: 2018 M. Shulhan <ms@kilabit.info>
-//
// SPDX-License-Identifier: BSD-3-Clause
+// SPDX-FileCopyrightText: 2018 M. Shulhan <ms@kilabit.info>
package http
@@ -869,7 +868,8 @@ func TestServer_HandleFS(t *testing.T) {
t.Fatal(err)
}
- var root = t.TempDir()
+ root := `testdata/Server_HandleFS`
+
var mfsOpts = memfs.Options{
Root: root,
MaxFileSize: -1,
@@ -892,48 +892,90 @@ func TestServer_HandleFS(t *testing.T) {
t.Fatal(err)
}
- var redactTime = regexp.MustCompile(`\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\d`)
- var redactedTime = []byte(`0000-00-00T00:00:00`)
- var simreq = libhttptest.SimulateRequest{
- Path: `/`,
- }
- var simres *libhttptest.SimulateResult
- var exp string
- var got []byte
+ listCase := []struct {
+ path string
+ expResponse string
+ }{{
+ path: `/`,
+ expResponse: string(tdata.Output[`/`]),
+ }, {
+ path: `/index`,
+ expResponse: string(tdata.Output[`/`]),
+ }, {
+ path: `/dir`,
+ expResponse: string(tdata.Output[`/dir`]),
+ }, {
+ path: `/dir?q=abc`,
+ expResponse: string(tdata.Output[`/dir?q=abc`]),
+ }, {
+ path: `/dir?q=abc#fgh`,
+ expResponse: string(tdata.Output[`/dir?q=abc#fgh`]),
+ }, {
+ path: `/dir#fgh`,
+ expResponse: string(tdata.Output[`/dir/#fgh`]),
+ }, {
+ path: `/dir/#fgh`,
+ expResponse: string(tdata.Output[`/dir/#fgh`]),
+ }, {
+ path: `/dir/`,
+ expResponse: string(tdata.Output[`/dir/`]),
+ }, {
+ path: `/b`,
+ expResponse: string(tdata.Output[`/b`]),
+ }, {
+ path: `/b?q=abc`,
+ expResponse: string(tdata.Output[`/b`]),
+ }, {
+ path: `/b?q=abc#fgh`,
+ expResponse: string(tdata.Output[`/b`]),
+ }}
+
+ for _, tc := range listCase {
+ simreq := libhttptest.SimulateRequest{
+ Path: tc.path,
+ }
- t.Run(`OnEmptyRoot`, func(t *testing.T) {
+ var simres *libhttptest.SimulateResult
simres, err = libhttptest.Simulate(httpd.ServeHTTP, &simreq)
if err != nil {
t.Fatal(err)
}
- exp = string(tdata.Output[t.Name()])
+ var got []byte
got, err = simres.DumpResponse([]string{HeaderETag})
if err != nil {
t.Fatal(err)
}
- test.Assert(t, `response`, exp, string(got))
- })
+ test.Assert(t, tc.path, tc.expResponse, string(got))
+ }
+
+ newDir := filepath.Join(root, `newDir`)
+ _ = os.RemoveAll(newDir)
+
+ t.Run(`OnNewDirectory`, func(tt *testing.T) {
+ redactTime := regexp.MustCompile(`\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\d`)
+ redactedTime := []byte(`0000-00-00T00:00:00`)
- t.Run(`OnNewDirectory`, func(t *testing.T) {
- var newDir = filepath.Join(root, `dirA`)
err = os.MkdirAll(newDir, 0755)
if err != nil {
- t.Fatal(err)
+ tt.Fatal(err)
}
- simres, err = libhttptest.Simulate(httpd.ServeHTTP, &simreq)
+ simreq := libhttptest.SimulateRequest{
+ Path: `/newDir/`,
+ }
+ simres, err := libhttptest.Simulate(httpd.ServeHTTP, &simreq)
if err != nil {
- t.Fatal(err)
+ tt.Fatal(err)
}
- exp = string(tdata.Output[t.Name()])
- got, err = simres.DumpResponse([]string{HeaderETag})
+ exp := string(tdata.Output[tt.Name()])
+ got, err := simres.DumpResponse([]string{HeaderETag})
if err != nil {
- t.Fatal(err)
+ tt.Fatal(err)
}
got = redactTime.ReplaceAll(got, redactedTime)
- test.Assert(t, `response`, exp, string(got))
+ test.Assert(tt, tt.Name(), exp, string(got))
})
}
diff --git a/lib/http/testdata/Server_HandleFS/b.html b/lib/http/testdata/Server_HandleFS/b.html
new file mode 100644
index 00000000..aa8d367a
--- /dev/null
+++ b/lib/http/testdata/Server_HandleFS/b.html
@@ -0,0 +1,6 @@
+<!doctype html>
+<html>
+ <head>
+ <title>/b.html</title>
+ </head>
+</html>
diff --git a/lib/http/testdata/Server_HandleFS/dir/index.html b/lib/http/testdata/Server_HandleFS/dir/index.html
new file mode 100644
index 00000000..245518a0
--- /dev/null
+++ b/lib/http/testdata/Server_HandleFS/dir/index.html
@@ -0,0 +1,6 @@
+<!doctype html>
+<html>
+ <head>
+ <title>/dir/index.html</title>
+ </head>
+</html>
diff --git a/lib/http/testdata/Server_HandleFS/index.html b/lib/http/testdata/Server_HandleFS/index.html
new file mode 100644
index 00000000..73b9ab05
--- /dev/null
+++ b/lib/http/testdata/Server_HandleFS/index.html
@@ -0,0 +1,6 @@
+<!doctype html>
+<html>
+ <head>
+ <title>/index.html</title>
+ </head>
+</html>
diff --git a/lib/http/testdata/Server_HandleFS_test.txt b/lib/http/testdata/Server_HandleFS_test.txt
index 1089121a..8b2173ef 100644
--- a/lib/http/testdata/Server_HandleFS_test.txt
+++ b/lib/http/testdata/Server_HandleFS_test.txt
@@ -1,21 +1,93 @@
// SPDX-License-Identifier: BSD-3-Clause
// SPDX-FileCopyrightText: 2025 M. Shulhan <ms@kilabit.info>
-<<< TestServer_HandleFS/OnEmptyRoot
+<<< /
HTTP/1.1 200 OK
Connection: close
Content-Type: text/html; charset=utf-8
-<!DOCTYPE html><html>
-<head>
-<meta name="viewport" content="width=device-width">
-<style>
-body{font-family:monospace; white-space:pre;}
-</style>
-</head>
-<body>
-<h3>Index of /</h3>
-</body></html>
+<!doctype html>
+<html>
+ <head>
+ <title>/index.html</title>
+ </head>
+</html>
+
+<<< /dir
+HTTP/1.1 302 Found
+Connection: close
+Content-Type: text/html; charset=utf-8
+Location: /dir/
+
+<a href="/dir/">Found</a>.
+
+
+
+<<< /dir?q=abc
+HTTP/1.1 302 Found
+Connection: close
+Content-Type: text/html; charset=utf-8
+Location: /dir/?q=abc
+
+<a href="/dir/?q=abc">Found</a>.
+
+
+
+<<< /dir?q=abc#fgh
+HTTP/1.1 302 Found
+Connection: close
+Content-Type: text/html; charset=utf-8
+Location: /dir/?q=abc#fgh
+
+<a href="/dir/?q=abc#fgh">Found</a>.
+
+
+
+<<< /dir/#fgh
+HTTP/1.1 404 Not Found
+Connection: close
+
+
+
+<<< /dir/
+HTTP/1.1 200 OK
+Connection: close
+Content-Type: text/html; charset=utf-8
+
+<!doctype html>
+<html>
+ <head>
+ <title>/dir/index.html</title>
+ </head>
+</html>
+
+
+<<< /b
+HTTP/1.1 200 OK
+Connection: close
+Content-Type: text/html; charset=utf-8
+
+<!doctype html>
+<html>
+ <head>
+ <title>/b.html</title>
+ </head>
+</html>
+
+
+<<< /b?q=abc
+HTTP/1.1 200 OK
+Connection: close
+Content-Type: text/html; charset=utf-8
+
+<!doctype html>
+<html>
+ <head>
+ <title>/b.html</title>
+ </head>
+</html>
+
+
<<< TestServer_HandleFS/OnNewDirectory
HTTP/1.1 200 OK
@@ -30,6 +102,6 @@ body{font-family:monospace; white-space:pre;}
</style>
</head>
<body>
-<h3>Index of /</h3>
-<div>drwxr-xr-x <tt> 0</tt> 0000-00-00T00:00:00Z <a href="/dirA">dirA</a></div><br/>
+<h3>Index of newDir</h3>
+<div><a href='..'>..</a></div><br/>
</body></html>