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/xml"
|
||||
"fmt"
|
||||
"html"
|
||||
"net/url"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
@ -115,7 +117,7 @@ func generateHTMLIndex(ctx *fasthttp.RequestCtx, reqPath string, entries []dirEn
|
||||
// HTML 头部
|
||||
buf.WriteString("<!DOCTYPE html>\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("body { font-family: monospace; margin: 20px; }\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("</style>\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("<thead><tr><th>Name</th><th>Modified</th><th>Size</th></tr></thead>\n")
|
||||
buf.WriteString("<tbody>\n")
|
||||
@ -140,7 +142,7 @@ func generateHTMLIndex(ctx *fasthttp.RequestCtx, reqPath string, entries []dirEn
|
||||
for _, entry := range entries {
|
||||
name := entry.Name
|
||||
displayName := name
|
||||
href := escapeURL(name)
|
||||
href := url.PathEscape(name)
|
||||
|
||||
if entry.IsDir {
|
||||
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",
|
||||
href, escapeHTML(displayName), timeStr, sizeStr))
|
||||
href, html.EscapeString(displayName), timeStr, sizeStr))
|
||||
}
|
||||
|
||||
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.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
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
@ -233,42 +234,25 @@ func TestFormatSize(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestEscapeHTML(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{"normal", "normal"},
|
||||
{"<script>", "<script>"},
|
||||
{`"quoted"`, ""quoted""},
|
||||
{"a&b", "a&b"},
|
||||
{"'single'", "'single'"},
|
||||
// TestGenerateAutoIndex_EmptyDirectory 测试空目录
|
||||
func TestGenerateAutoIndex_EmptyDirectory(t *testing.T) {
|
||||
tmpDir, err := os.MkdirTemp("", "autoindex_empty_test")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
ctx := &fasthttp.RequestCtx{}
|
||||
config := AutoIndexConfig{Format: "json"}
|
||||
|
||||
if !GenerateAutoIndex(ctx, tmpDir, "/", config) {
|
||||
t.Fatal("GenerateAutoIndex returned false for empty directory")
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
result := escapeHTML(tt.input)
|
||||
if result != tt.expected {
|
||||
t.Errorf("escapeHTML(%q) = %q, want %q", tt.input, result, tt.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
body := string(ctx.Response.Body())
|
||||
// JSON 格式空数组
|
||||
if !containsAll(body, "[]") {
|
||||
t.Errorf("Empty directory JSON = %s, should contain []", body)
|
||||
}
|
||||
}
|
||||
|
||||
@ -421,7 +405,7 @@ func BenchmarkGenerateAutoIndex_HTML(b *testing.B) {
|
||||
|
||||
// 创建 100 个文件
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user