refactor(handler): improve autoindex code quality
- Replace custom escape functions with stdlib html.EscapeString and url.PathEscape - Fix benchmark test file naming using fmt.Sprintf - Add CSP security header for HTML output - Add empty directory test case - Remove obsolete escape function tests Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
b62a3f12da
commit
3b608be0de
@ -11,6 +11,8 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"html"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
@ -115,7 +117,7 @@ func generateHTMLIndex(ctx *fasthttp.RequestCtx, reqPath string, entries []dirEn
|
|||||||
// HTML 头部
|
// HTML 头部
|
||||||
buf.WriteString("<!DOCTYPE html>\n")
|
buf.WriteString("<!DOCTYPE html>\n")
|
||||||
buf.WriteString("<html>\n<head>\n")
|
buf.WriteString("<html>\n<head>\n")
|
||||||
buf.WriteString(fmt.Sprintf("<title>Index of %s</title>\n", escapeHTML(reqPath)))
|
buf.WriteString(fmt.Sprintf("<title>Index of %s</title>\n", html.EscapeString(reqPath)))
|
||||||
buf.WriteString("<style>\n")
|
buf.WriteString("<style>\n")
|
||||||
buf.WriteString("body { font-family: monospace; margin: 20px; }\n")
|
buf.WriteString("body { font-family: monospace; margin: 20px; }\n")
|
||||||
buf.WriteString("h1 { border-bottom: 1px solid #ccc; padding-bottom: 10px; }\n")
|
buf.WriteString("h1 { border-bottom: 1px solid #ccc; padding-bottom: 10px; }\n")
|
||||||
@ -126,7 +128,7 @@ func generateHTMLIndex(ctx *fasthttp.RequestCtx, reqPath string, entries []dirEn
|
|||||||
buf.WriteString("a:hover { text-decoration: underline; }\n")
|
buf.WriteString("a:hover { text-decoration: underline; }\n")
|
||||||
buf.WriteString("</style>\n")
|
buf.WriteString("</style>\n")
|
||||||
buf.WriteString("</head>\n<body>\n")
|
buf.WriteString("</head>\n<body>\n")
|
||||||
buf.WriteString(fmt.Sprintf("<h1>Index of %s</h1>\n", escapeHTML(reqPath)))
|
buf.WriteString(fmt.Sprintf("<h1>Index of %s</h1>\n", html.EscapeString(reqPath)))
|
||||||
buf.WriteString("<hr>\n<table>\n")
|
buf.WriteString("<hr>\n<table>\n")
|
||||||
buf.WriteString("<thead><tr><th>Name</th><th>Modified</th><th>Size</th></tr></thead>\n")
|
buf.WriteString("<thead><tr><th>Name</th><th>Modified</th><th>Size</th></tr></thead>\n")
|
||||||
buf.WriteString("<tbody>\n")
|
buf.WriteString("<tbody>\n")
|
||||||
@ -140,7 +142,7 @@ func generateHTMLIndex(ctx *fasthttp.RequestCtx, reqPath string, entries []dirEn
|
|||||||
for _, entry := range entries {
|
for _, entry := range entries {
|
||||||
name := entry.Name
|
name := entry.Name
|
||||||
displayName := name
|
displayName := name
|
||||||
href := escapeURL(name)
|
href := url.PathEscape(name)
|
||||||
|
|
||||||
if entry.IsDir {
|
if entry.IsDir {
|
||||||
displayName += "/"
|
displayName += "/"
|
||||||
@ -166,11 +168,12 @@ func generateHTMLIndex(ctx *fasthttp.RequestCtx, reqPath string, entries []dirEn
|
|||||||
}
|
}
|
||||||
|
|
||||||
buf.WriteString(fmt.Sprintf("<tr><td><a href=\"%s\">%s</a></td><td>%s</td><td class=\"size\">%s</td></tr>\n",
|
buf.WriteString(fmt.Sprintf("<tr><td><a href=\"%s\">%s</a></td><td>%s</td><td class=\"size\">%s</td></tr>\n",
|
||||||
href, escapeHTML(displayName), timeStr, sizeStr))
|
href, html.EscapeString(displayName), timeStr, sizeStr))
|
||||||
}
|
}
|
||||||
|
|
||||||
buf.WriteString("</tbody>\n</table>\n<hr>\n</body>\n</html>\n")
|
buf.WriteString("</tbody>\n</table>\n<hr>\n</body>\n</html>\n")
|
||||||
|
|
||||||
|
ctx.Response.Header.Set("Content-Security-Policy", "default-src 'self'")
|
||||||
ctx.Response.Header.SetContentType("text/html; charset=utf-8")
|
ctx.Response.Header.SetContentType("text/html; charset=utf-8")
|
||||||
ctx.Response.SetBody(buf.Bytes())
|
ctx.Response.SetBody(buf.Bytes())
|
||||||
}
|
}
|
||||||
@ -275,27 +278,3 @@ func formatSize(size int64) string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// escapeHTML 转义 HTML 特殊字符。
|
|
||||||
func escapeHTML(s string) string {
|
|
||||||
s = strings.ReplaceAll(s, "&", "&")
|
|
||||||
s = strings.ReplaceAll(s, "<", "<")
|
|
||||||
s = strings.ReplaceAll(s, ">", ">")
|
|
||||||
s = strings.ReplaceAll(s, "\"", """)
|
|
||||||
s = strings.ReplaceAll(s, "'", "'")
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
// escapeURL 转义 URL 特殊字符。
|
|
||||||
func escapeURL(s string) string {
|
|
||||||
var buf bytes.Buffer
|
|
||||||
for _, c := range s {
|
|
||||||
// 保留安全字符
|
|
||||||
if (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') ||
|
|
||||||
c == '-' || c == '_' || c == '.' || c == '~' || c == '/' {
|
|
||||||
buf.WriteRune(c)
|
|
||||||
} else {
|
|
||||||
buf.WriteString(fmt.Sprintf("%%%02X", c))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return buf.String()
|
|
||||||
}
|
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
package handler
|
package handler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
@ -233,42 +234,25 @@ func TestFormatSize(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestEscapeHTML(t *testing.T) {
|
// TestGenerateAutoIndex_EmptyDirectory 测试空目录
|
||||||
tests := []struct {
|
func TestGenerateAutoIndex_EmptyDirectory(t *testing.T) {
|
||||||
input string
|
tmpDir, err := os.MkdirTemp("", "autoindex_empty_test")
|
||||||
expected string
|
if err != nil {
|
||||||
}{
|
t.Fatal(err)
|
||||||
{"normal", "normal"},
|
}
|
||||||
{"<script>", "<script>"},
|
defer os.RemoveAll(tmpDir)
|
||||||
{`"quoted"`, ""quoted""},
|
|
||||||
{"a&b", "a&b"},
|
ctx := &fasthttp.RequestCtx{}
|
||||||
{"'single'", "'single'"},
|
config := AutoIndexConfig{Format: "json"}
|
||||||
|
|
||||||
|
if !GenerateAutoIndex(ctx, tmpDir, "/", config) {
|
||||||
|
t.Fatal("GenerateAutoIndex returned false for empty directory")
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
body := string(ctx.Response.Body())
|
||||||
result := escapeHTML(tt.input)
|
// JSON 格式空数组
|
||||||
if result != tt.expected {
|
if !containsAll(body, "[]") {
|
||||||
t.Errorf("escapeHTML(%q) = %q, want %q", tt.input, result, tt.expected)
|
t.Errorf("Empty directory JSON = %s, should contain []", body)
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEscapeURL(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
input string
|
|
||||||
expected string
|
|
||||||
}{
|
|
||||||
{"normal", "normal"},
|
|
||||||
{"file name.txt", "file%20name.txt"},
|
|
||||||
{"file?name.txt", "file%3Fname.txt"},
|
|
||||||
{"safe-file_123.txt", "safe-file_123.txt"},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
result := escapeURL(tt.input)
|
|
||||||
if result != tt.expected {
|
|
||||||
t.Errorf("escapeURL(%q) = %q, want %q", tt.input, result, tt.expected)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -421,7 +405,7 @@ func BenchmarkGenerateAutoIndex_HTML(b *testing.B) {
|
|||||||
|
|
||||||
// 创建 100 个文件
|
// 创建 100 个文件
|
||||||
for i := 0; i < 100; i++ {
|
for i := 0; i < 100; i++ {
|
||||||
if err := os.WriteFile(filepath.Join(tmpDir, "file"+string(rune('0'+i%10))+string(rune('0'+i/10))+".txt"), []byte("content"), 0o644); err != nil {
|
if err := os.WriteFile(filepath.Join(tmpDir, fmt.Sprintf("file%d.txt", i)), []byte("content"), 0o644); err != nil {
|
||||||
b.Fatal(err)
|
b.Fatal(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user