权限与功能改进
This commit is contained in:
101
afi.go
101
afi.go
@@ -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服务
|
||||
|
||||
Reference in New Issue
Block a user