移动设备改进

This commit is contained in:
2026-04-05 05:11:10 +08:00
parent 3206519910
commit 91a71dd6a5
4 changed files with 125 additions and 67 deletions

88
afi.go
View File

@@ -31,6 +31,8 @@ import (
"time"
)
const ver = "26.04.03"
// rowTemplate 定义文件列表中的每一行数据结构
type rowTemplate struct {
Name string // 文件/文件夹名称
@@ -53,15 +55,15 @@ type pageTemplate struct {
}
// 命令行参数定义
var host = flag.String("h", "127.0.0.1", "监听的主机地址") // 监听的主机地址
var port = flag.String("p", "8001", "监听的端口") // 监听的端口
var extraPath = flag.String("prefix", "/", "afi 的 URL 访问路径前缀, 例如 /afi/ (斜杠很重要)") // URL前缀路径
var symlinks = flag.Bool("symlinks", false, "跟随符号链接 \033[4m警告\033[0m: 符号链接可能会跳出已定义的\"根目录\" (默认值: false)") // 是否跟随符号链接
var verb = flag.Bool("verb", false, "详细输出") // 是否输出详细日志
var skipHidden = flag.Bool("k", true, "\n跳过隐藏文件") // 是否跳过隐藏文件(以.开头)
var host = flag.String("h", "127.0.0.1", "监听的主机地址") // 监听的主机地址
var port = flag.String("p", "8001", "监听的端口") // 监听的端口
var extraPath = flag.String("prefix", "/", "afi 的 URL 访问路径前缀, 例如 /afi/ (斜杠很重要)") // URL前缀路径
var symlinks = flag.Bool("symlinks", false, "跟随符号链接 警告: 启用后符号链接可能会跳出已定义的\"根目录\" (default false)") // 是否跟随符号链接
var verb = flag.Bool("verb", false, "详细输出") // 是否输出详细日志
var skipHidden = flag.Bool("k", true, "\n跳过隐藏文件") // 是否跳过隐藏文件(以.开头)
// var ro = flag.Bool("ro", false, "只读模式(无法修改文件系统)") // 是否只读模式
var title = flag.String("title", "%PATH%", "页面标题, 用%PATH%指代完整路径, %ITEM%指代末端文件/目录名, 不会泄根目录目录名")
var authstr = flag.String("auth", `{"admin": "password"}`, "可写用户的认证数据") // 如果非要安全 应该用stdin注入密码防盗窃, 这个flag只能用来调试 不然服务器一个ps就泄露了
var title = flag.String("title", "%PATH%", "页面标题, 用 %PATH% 指代完整路径, %ITEM% 指代末端文件/目录名, 不会泄根目录目录名")
var authstr = flag.String("auth", `{"admin": "password"}`, "可写用户的认证数据 (也可以用环境变量 AFI_AUTH 设置, AFI_AUTH 是最优先且最安全的)") // 这个flag只能用来调试 不然服务器一个ps就泄露了
// rpcCall 定义RPC调用的JSON数据结构
// 用于处理前端发送的远程调用请求(创建目录、移动、删除、计算校验和)
@@ -256,7 +258,7 @@ func replyList(w http.ResponseWriter, r *http.Request, fullPath string, path str
// 根据类型添加到不同的列表
if el.IsDir() {
row := rowTemplate{name + "/", template.URL(href), "4.0kB", "folder", desc, "inode/directory"}
row := rowTemplate{html.EscapeString(name + "/"), template.URL(href), "4.0kB", "folder", html.EscapeString(desc), "inode/directory"}
p.RowsFolders = append(p.RowsFolders, row)
} else {
// 提取文件扩展名
@@ -270,7 +272,7 @@ func replyList(w http.ResponseWriter, r *http.Request, fullPath string, path str
filemime = "unknown/unknown"
}
}
row := rowTemplate{name, template.URL(href), humanize(el.Size()), ext, desc, filemime}
row := rowTemplate{html.EscapeString(name), template.URL(href), humanize(el.Size()), html.EscapeString(ext), html.EscapeString(desc), filemime}
p.RowsFiles = append(p.RowsFiles, row)
}
}
@@ -337,34 +339,34 @@ func checkAuthRequest(w http.ResponseWriter, r *http.Request) bool {
}
// 拒绝现代JWT, 古法auth, 我们有先进的https
// 这样不能防 XSS
// 这样不利好防御 XSS 虽说已经把模板搞安全了
// 但是至于XSS, 用户都在浏览器随便运行tampermonkey了我还能说什么呢, 说不定是用户想写脚本调试呢, 我把xss预防了让他们费劲去找token反而不好
// 这才叫利好curl, 而且还防 CSRF
func authLogin(w http.ResponseWriter, r *http.Request) {
username, password, ok := r.BasicAuth()
if !ok {
w.Header().Set("WWW-Authenticate", `Basic realm="AFI File Manager"`)
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
if checkAuth(username, password) {
http.Redirect(w, r, *extraPath, http.StatusFound)
} else {
w.Header().Set("WWW-Authenticate", `Basic realm="AFI File Manager"`)
http.Error(w, "Unauthorized", http.StatusUnauthorized)
}
username, password, ok := r.BasicAuth()
if !ok {
w.Header().Set("WWW-Authenticate", `Basic realm="AFI File Manager"`)
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
if checkAuth(username, password) {
http.Redirect(w, r, *extraPath, http.StatusFound)
} else {
w.Header().Set("WWW-Authenticate", `Basic realm="AFI File Manager"`)
http.Error(w, "Unauthorized", http.StatusUnauthorized)
}
}
func authState(w http.ResponseWriter, r *http.Request) {
username, password, ok := r.BasicAuth()
if !ok {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
if checkAuth(username, password) {
w.WriteHeader(http.StatusOK)
} else {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
}
username, password, ok := r.BasicAuth()
if !ok {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
if checkAuth(username, password) {
w.WriteHeader(http.StatusOK)
} else {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
}
}
// upload 处理文件上传请求
@@ -540,7 +542,11 @@ func enforcePath(p string) string {
func main() {
// 解析命令行参数
if flag.Parse(); len(flag.Args()) == 1 {
flag.Parse()
if envAuth := os.Getenv("AFI_AUTH"); envAuth != "" {
*authstr = envAuth
}
if len(flag.Args()) == 1 {
rootPath = flag.Args()[0]
} else {
fmt.Printf("Usage: ./afi [OPTION]... [ROOTDIR]\n\n")
@@ -558,18 +564,18 @@ func main() {
server := &http.Server{Addr: *host + ":" + *port, Handler: handler}
// 注册路由处理器
http.HandleFunc(*extraPath+"rpc", rpc) // RPC接口 (增删移与校验和)
http.HandleFunc(*extraPath+"post", upload) // 文件上传接口
http.HandleFunc(*extraPath+"dav", upload) // TODO: webdav 接口
http.HandleFunc(*extraPath+"auth", authState) // 仅检查登录接口(不会弹窗)
http.HandleFunc(*extraPath+"rpc", rpc) // RPC接口 (增删移与校验和)
http.HandleFunc(*extraPath+"post", upload) // 文件上传接口
http.HandleFunc(*extraPath+"dav", upload) // TODO: webdav 接口
http.HandleFunc(*extraPath+"auth", authState) // 仅检查登录接口(不会弹窗)
http.HandleFunc(*extraPath+"login", authLogin) // 登录接口
http.HandleFunc(*extraPath+"zip", zipRPC) // ZIP打包下载接口
http.HandleFunc("/", doContent) // 主内容处理接口
http.HandleFunc(*extraPath+"zip", zipRPC) // ZIP打包下载接口
http.HandleFunc("/", doContent) // 主内容处理接口
// 创建静态文件服务器, 用于直接提供文件下载
handler = http.StripPrefix(*extraPath, http.FileServer(http.Dir(rootPath)))
// 输出启动信息
fmt.Printf("Agile File Indexer\n")
fmt.Printf("Agile File Indexer 版本 %s\n", ver)
fmt.Printf("AFI 是 Gossa 的增强分支, 但是并不完全向下兼容原版 Gossa 的参数\n")
fmt.Printf("AFI 已启动, 根目录为 %s\n", rootPath)
fmt.Printf("详细输出: %t, 符号链接跟随: %t, 跳过隐藏文件: %t\n", *verb, *symlinks, *skipHidden)