权限与功能改进

This commit is contained in:
2026-04-04 09:45:27 +08:00
parent 4a55a4bc36
commit 3206519910
4 changed files with 276 additions and 116 deletions

101
afi.go
View File

@@ -32,7 +32,6 @@ import (
)
// rowTemplate 定义文件列表中的每一行数据结构
// 用于在HTML模板中渲染项目的显示信息
type rowTemplate struct {
Name string // 文件/文件夹名称
Href template.URL // 链接地址
@@ -43,7 +42,6 @@ type rowTemplate struct {
}
// pageTemplate 定义整个页面渲染所需的数据结构
// 包含页面标题、路径前缀、读写模式标志以及文件和文件夹列表
type pageTemplate struct {
Title string // 页面标题
Path template.HTML // 页面路径
@@ -61,8 +59,9 @@ var extraPath = flag.String("prefix", "/", "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 ro = flag.Bool("ro", false, "只读模式(无法修改文件系统)") // 是否只读模式
// var ro = flag.Bool("ro", false, "只读模式(无法修改文件系统)") // 是否只读模式
var title = flag.String("title", "%PATH%", "页面标题, 用%PATH%指代完整路径, %ITEM%指代末端文件/目录名, 不会泄漏根目录目录名")
var authstr = flag.String("auth", `{"admin": "password"}`, "可写用户的认证数据") // 如果非要安全 应该用stdin注入密码防盗窃, 这个flag只能用来调试 不然服务器一个ps就泄露了
// rpcCall 定义RPC调用的JSON数据结构
// 用于处理前端发送的远程调用请求(创建目录、移动、删除、计算校验和)
@@ -71,8 +70,9 @@ type rpcCall struct {
Args []string `json:"args"` // 方法参数
}
var rootPath = "" // 共享目录的根路径绝对路径
var rootPath = "" // 共享目录的根路径(文件系统中的绝对路径)
var handler http.Handler // HTTP处理器, 用于处理静态文件服务
var authTable = make(map[string]string)
// check 检查错误, 如果错误不为nil则panic
// 用于简化错误处理
@@ -91,7 +91,6 @@ func exitPath(w http.ResponseWriter, s ...interface{}) {
// 发生panic时记录错误并返回500状态码
log.Println("error", s, r)
w.WriteHeader(500)
w.Write([]byte("error"))
} else if *verb {
// 详细模式下记录正常日志
log.Println(s...)
@@ -220,7 +219,6 @@ func replyList(w http.ResponseWriter, r *http.Request, fullPath string, path str
p.RowsFolders = append(p.RowsFolders, rowTemplate{"../", "..", "4.0kB", "folder", "返回上级目录", "inode/directory"})
}
p.ExtraPath = template.HTML(html.EscapeString(*extraPath))
p.Ro = *ro
p.Path = template.HTML(html.EscapeString(pathtext)) // 安全化路径
if pathtext == "/" {
p.Title = strings.Replace(strings.Replace(*title, "%PATH%", pathtext, -1), "%ITEM%", "/", -1)
@@ -296,9 +294,8 @@ func replyList(w http.ResponseWriter, r *http.Request, fullPath string, path str
// w: HTTP响应写入器
// r: HTTP请求
func doContent(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query()
name := query.Get("header")
name := query.Get("footer")
// query := r.URL.Query()
// 此处可以接收参数
// 如果URL不以配置的前缀开头, 重定向到正确的前缀
if !strings.HasPrefix(r.URL.Path, *extraPath) {
http.Redirect(w, r, *extraPath, http.StatusFound)
@@ -321,11 +318,63 @@ func doContent(w http.ResponseWriter, r *http.Request) {
}
}
func checkAuth(username string, password string) bool {
return authTable[username] == password
// WARN: 这么搞不能防御 timing attack
// 切勿用于生产环境
}
func checkAuthRequest(w http.ResponseWriter, r *http.Request) bool {
username, password, ok := r.BasicAuth()
if !ok {
return false
}
if checkAuth(username, password) {
return true
} else {
return false
}
}
// 拒绝现代JWT, 古法auth, 我们有先进的https
// 这样不能防 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)
}
}
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)
}
}
// upload 处理文件上传请求
// 从multipart请求中读取文件内容并保存到指定路径
// w: HTTP响应写入器
// r: HTTP请求
func upload(w http.ResponseWriter, r *http.Request) {
if !checkAuthRequest(w, r) {
return
}
// 从header中获取目标路径
path := r.Header.Get("afi-path")
defer exitPath(w, "upload", path)
@@ -352,6 +401,9 @@ func upload(w http.ResponseWriter, r *http.Request) {
// w: HTTP响应写入器
// r: HTTP请求
func zipRPC(w http.ResponseWriter, r *http.Request) {
if !checkAuthRequest(w, r) {
return
}
// 获取要打包的路径和ZIP文件名
zipPath := r.URL.Query().Get("zipPath")
zipName := r.URL.Query().Get("zipName")
@@ -389,7 +441,7 @@ func zipRPC(w http.ResponseWriter, r *http.Request) {
header, err := zip.FileInfoHeader(f)
check(err)
header.Name = filepath.ToSlash(rel) // 统一使用斜杠分隔
header.Method = zip.Store // 使用存储模式不压缩
header.Method = zip.Store // 使用存储模式 (不压缩)
headerWriter, err := zipWriter.CreateHeader(header)
check(err)
// 打开文件并复制到ZIP中
@@ -409,6 +461,9 @@ func zipRPC(w http.ResponseWriter, r *http.Request) {
// w: HTTP响应写入器
// r: HTTP请求
func rpc(w http.ResponseWriter, r *http.Request) {
if !checkAuthRequest(w, r) {
return
}
var err error
var rpc rpcCall
defer exitPath(w, "rpc", &rpc)
@@ -488,33 +543,37 @@ func main() {
if flag.Parse(); len(flag.Args()) == 1 {
rootPath = flag.Args()[0]
} else {
fmt.Printf("\nusage: ./afi [OPTIONS] ~/directory-to-share\n\n")
fmt.Printf("Usage: ./afi [OPTION]... [ROOTDIR]\n\n")
flag.PrintDefaults()
os.Exit(1)
}
err := json.Unmarshal([]byte(*authstr), &authTable)
check(err)
// 获取共享目录的绝对路径
var err error
rootPath, err = filepath.Abs(rootPath)
check(err)
// 创建HTTP服务器
server := &http.Server{Addr: *host + ":" + *port, Handler: handler}
// 注册路由处理器(非只读模式下启用修改操作)
if !*ro {
http.HandleFunc(*extraPath+"rpc", rpc) // RPC接口创建目录、移动、删除、校验和
http.HandleFunc(*extraPath+"post", upload) // 文件上传接口
}
http.HandleFunc(*extraPath+"zip", zipRPC) // ZIP打包下载接口
http.HandleFunc("/", doContent) // 主内容处理接口
// 注册路由处理器
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) // 主内容处理接口
// 创建静态文件服务器, 用于直接提供文件下载
handler = http.StripPrefix(*extraPath, http.FileServer(http.Dir(rootPath)))
// 输出启动信息
fmt.Printf("Agile File Indexer 是 Gossa 的分支\n")
fmt.Printf("Agile File Indexer\n")
fmt.Printf("AFI 是 Gossa 的增强分支, 但是并不完全向下兼容原版 Gossa 的参数\n")
fmt.Printf("AFI 已启动, 根目录为 %s\n", rootPath)
fmt.Printf("详细输出: %t, 符号链接跟随: %t, 只读模式: %t, 跳过隐藏文件: %t\n", *verb, *symlinks, *ro, *skipHidden)
fmt.Printf("详细输出: %t, 符号链接跟随: %t, 跳过隐藏文件: %t\n", *verb, *symlinks, *skipHidden)
fmt.Printf("身份验证信息: %s\n", *authstr)
fmt.Printf("正在监听: http://%s:%s%s\n", *host, *port, *extraPath)
// 启动HTTP服务