- internal/auth/mode.go:Mode 解析/归一化,X-Auth-User 头常量 - server:adminAuth 按模式鉴权,sso 只认网关注入的 X-Auth-User(缺失 401 JSON) - 新增公开 /api/admin/mode 供前端识别模式;sso 下 login/logout 返回 404 - main:读取 AUTH_MODE,sso 时跳过自带口令并打印启动提示 - 测试覆盖两种模式、伪造 header 不绕过、数据面不受影响
158 lines
3.7 KiB
Go
158 lines
3.7 KiB
Go
// Package server 负责路由装配:WebDAV、admin API、client API 与静态资源。
|
|
package server
|
|
|
|
import (
|
|
"encoding/json"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"golang.org/x/net/webdav"
|
|
|
|
"github.com/YLing2024/davbox/internal/account"
|
|
"github.com/YLing2024/davbox/internal/auth"
|
|
)
|
|
|
|
// Config 是装配服务器所需的依赖。
|
|
type Config struct {
|
|
DataDir string
|
|
Store *account.Store
|
|
AdminHash []byte
|
|
Signer *auth.Signer
|
|
// AuthMode 是管理端认证模式;零值按 builtin 处理。
|
|
AuthMode auth.Mode
|
|
}
|
|
|
|
// Server 持有全部运行期状态。
|
|
type Server struct {
|
|
dataDir string
|
|
store *account.Store
|
|
adminHash []byte
|
|
signer *auth.Signer
|
|
authMode auth.Mode
|
|
|
|
davMu sync.Mutex
|
|
locks map[string]webdav.LockSystem
|
|
|
|
usage *usageCache
|
|
}
|
|
|
|
// New 构造服务器。
|
|
func New(cfg Config) *Server {
|
|
return &Server{
|
|
dataDir: cfg.DataDir,
|
|
store: cfg.Store,
|
|
adminHash: cfg.AdminHash,
|
|
signer: cfg.Signer,
|
|
authMode: cfg.AuthMode.Normalize(),
|
|
locks: map[string]webdav.LockSystem{},
|
|
usage: newUsageCache(30 * time.Second),
|
|
}
|
|
}
|
|
|
|
// Handler 返回顶层 http.Handler。
|
|
func (s *Server) Handler() http.Handler {
|
|
return http.HandlerFunc(s.route)
|
|
}
|
|
|
|
func (s *Server) route(w http.ResponseWriter, r *http.Request) {
|
|
decoded := r.URL.Path
|
|
if unsafePath(r.URL.EscapedPath(), decoded) {
|
|
writeError(w, http.StatusBadRequest, "请求路径不合法")
|
|
return
|
|
}
|
|
|
|
switch {
|
|
case decoded == "/api/admin" || strings.HasPrefix(decoded, "/api/admin/"):
|
|
s.handleAdmin(w, r)
|
|
case decoded == "/api/client" || strings.HasPrefix(decoded, "/api/client/"):
|
|
s.handleClient(w, r)
|
|
case decoded == "/" || decoded == "/admin" || decoded == "/admin/":
|
|
s.serveIndex(w, r)
|
|
case strings.HasPrefix(decoded, "/assets/"):
|
|
s.serveStatic(w, r)
|
|
default:
|
|
if seg := firstSegment(decoded); seg != "" {
|
|
if _, ok := s.store.Get(seg); ok {
|
|
s.handleDAV(w, r, seg)
|
|
return
|
|
}
|
|
// 账号不存在时返回与未认证一致的 401,避免泄漏账号是否存在。
|
|
writeAuthChallenge(w)
|
|
return
|
|
}
|
|
writeError(w, http.StatusNotFound, "未找到")
|
|
}
|
|
}
|
|
|
|
func firstSegment(p string) string {
|
|
p = strings.TrimPrefix(p, "/")
|
|
if i := strings.IndexByte(p, '/'); i >= 0 {
|
|
return p[:i]
|
|
}
|
|
return p
|
|
}
|
|
|
|
// unsafePath 拒绝含 `..`、编码后的 `%2e`、反斜杠、空字节的路径。
|
|
func unsafePath(escaped, decoded string) bool {
|
|
low := strings.ToLower(escaped)
|
|
for _, bad := range []string{"%2e", "%5c", "%00"} {
|
|
if strings.Contains(low, bad) {
|
|
return true
|
|
}
|
|
}
|
|
if strings.Contains(decoded, "..") || strings.Contains(decoded, `\`) || strings.ContainsRune(decoded, 0) {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
func writeJSON(w http.ResponseWriter, status int, v any) {
|
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
|
w.WriteHeader(status)
|
|
_ = json.NewEncoder(w).Encode(v)
|
|
}
|
|
|
|
func writeError(w http.ResponseWriter, status int, msg string) {
|
|
writeJSON(w, status, map[string]string{"error": msg})
|
|
}
|
|
|
|
func decodeJSON(r *http.Request, v any) error {
|
|
dec := json.NewDecoder(io.LimitReader(r.Body, 1<<20))
|
|
return dec.Decode(v)
|
|
}
|
|
|
|
type usageEntry struct {
|
|
bytes int64
|
|
files int64
|
|
exp time.Time
|
|
}
|
|
|
|
type usageCache struct {
|
|
mu sync.Mutex
|
|
ttl time.Duration
|
|
m map[string]usageEntry
|
|
}
|
|
|
|
func newUsageCache(ttl time.Duration) *usageCache {
|
|
return &usageCache{ttl: ttl, m: map[string]usageEntry{}}
|
|
}
|
|
|
|
func (c *usageCache) stat(key, root string, compute func(string) (int64, int64, error)) (int64, int64) {
|
|
c.mu.Lock()
|
|
if e, ok := c.m[key]; ok && time.Now().Before(e.exp) {
|
|
c.mu.Unlock()
|
|
return e.bytes, e.files
|
|
}
|
|
c.mu.Unlock()
|
|
|
|
bytes, files, _ := compute(root)
|
|
|
|
c.mu.Lock()
|
|
c.m[key] = usageEntry{bytes: bytes, files: files, exp: time.Now().Add(c.ttl)}
|
|
c.mu.Unlock()
|
|
return bytes, files
|
|
}
|