Files
WebDAV/internal/server/server.go
T
Yunling Zhang b3c7f04c2b feat(auth): 管理端新增 AUTH_MODE=builtin|sso,默认 builtin 行为不变
- 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 不绕过、数据面不受影响
2026-09-29 13:43:57 +08:00

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
}