aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-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>