package storage import ( "crypto/md5" "crypto/rand" "crypto/sha256" "encoding/hex" "fmt" "net/http" "strings" "sync" ) // authMode 认证模式(WebDAV 服务端挑战后自动协商)。 type authMode int const ( authModeUnknown authMode = iota // 未定:先发 Basic 探测 authModeBasic authModeDigest ) // authState WebDAV Basic/Digest 认证状态。 // // 策略: // - 首个请求预置 Basic;若服务端 401 且挑战为 Digest,则解析挑战参数切换为 Digest; // - Digest 按 RFC 2617/7616 实现 qop=auth(MD5 / SHA-256,含 -sess 变体); // qop 缺失时回退 RFC 2069 旧式响应; // - nonce 变更时重置 nc 计数;nc/cnonce 在互斥锁内生成保证并发唯一。 type authState struct { mu sync.Mutex username string password string mode authMode realm string nonce string qop string // 选定的 qop("auth" 或空 = RFC2069) opaque string algorithm string // MD5 | MD5-sess | SHA-256 | SHA-256-sess nc uint32 knownBasicOK bool // 已确认 Basic 可用 } // newAuthState 构造认证状态(默认以 Basic 起步)。 func newAuthState(username, password string) *authState { return &authState{username: username, password: password} } // apply 为请求设置 Authorization 头(每次请求调用,Digest 时消耗一个 nc)。 func (a *authState) apply(req *http.Request) { a.mu.Lock() defer a.mu.Unlock() switch { case a.mode == authModeDigest && a.nonce != "": req.Header.Set("Authorization", a.digestHeader(req)) default: req.SetBasicAuth(a.username, a.password) } } // digestHeader 依据缓存的挑战参数计算 Digest Authorization 头(调用方需持锁)。 func (a *authState) digestHeader(req *http.Request) string { uri := req.URL.RequestURI() method := strings.ToUpper(req.Method) ncStr := fmt.Sprintf("%08x", a.nc+1) a.nc++ cnonce := randomHex(8) var ha1 string switch strings.ToLower(a.algorithm) { case "md5-sess": ha1 = hashHex("md5", hashHex("md5", a.username+":"+a.realm+":"+a.password)+":"+a.nonce+":"+cnonce) case "sha-256-sess": ha1 = hashHex("sha256", hashHex("sha256", a.username+":"+a.realm+":"+a.password)+":"+a.nonce+":"+cnonce) case "sha-256": ha1 = hashHex("sha256", a.username+":"+a.realm+":"+a.password) default: // md5 ha1 = hashHex("md5", a.username+":"+a.realm+":"+a.password) } ha2 := hashHex(algoName(a.algorithm), method+":"+uri) var response string var fields []string esc := escapeDigestValue(a.username) if a.qop == "" { // RFC 2069 response = hashHex(algoName(a.algorithm), ha1+":"+a.nonce+":"+ha2) fields = append(fields, `Digest username="`+esc+`"`, `realm="`+escapeDigestValue(a.realm)+`"`, `nonce="`+escapeDigestValue(a.nonce)+`"`, `uri="`+escapeDigestValue(uri)+`"`, `response="`+response+`"`) } else { response = hashHex(algoName(a.algorithm), ha1+":"+a.nonce+":"+ncStr+":"+cnonce+":"+a.qop+":"+ha2) fields = append(fields, `Digest username="`+esc+`"`, `realm="`+escapeDigestValue(a.realm)+`"`, `nonce="`+escapeDigestValue(a.nonce)+`"`, `uri="`+escapeDigestValue(uri)+`"`, `cnonce="`+cnonce+`"`, `nc=`+ncStr, `qop=`+a.qop, `response="`+response+`"`, `algorithm=`+a.algorithm) } if a.opaque != "" { fields = append(fields, `opaque="`+escapeDigestValue(a.opaque)+`"`) } return strings.Join(fields, ", ") } // challenge 处理 401 的 WWW-Authenticate 挑战;返回是否已切换认证方式可重试。 // 返回 false 表示凭据错误或算法不受支持,调用方应直接报错。 func (a *authState) challenge(header string) bool { a.mu.Lock() defer a.mu.Unlock() h := strings.TrimSpace(header) lower := strings.ToLower(h) switch { case strings.HasPrefix(lower, "digest"): params := parseChallengeParams(strings.TrimPrefix(h[len("Digest"):], " ")) algo := strings.ToUpper(strings.TrimSpace(params["algorithm"])) if algo == "" { algo = "MD5" } switch algo { case "MD5", "MD5-SESS", "SHA-256", "SHA-256-SESS": default: return false // 不支持的摘要算法 } if params["nonce"] == "" || params["realm"] == "" { return false } qop := "" if raw := strings.TrimSpace(params["qop"]); raw != "" { for _, candidate := range strings.Split(raw, ",") { if strings.EqualFold(strings.TrimSpace(candidate), "auth") { qop = "auth" break } } if qop == "" { return false // 仅支持 auth-int 等需要 body 哈希的模式 } } if a.nonce != params["nonce"] { a.nc = 0 } a.realm, a.nonce, a.qop = params["realm"], params["nonce"], qop a.opaque, a.algorithm = params["opaque"], strings.ToLower(algo) a.mode = authModeDigest a.knownBasicOK = false return true case strings.HasPrefix(lower, "basic"): if a.knownBasicOK || a.mode == authModeBasic { return false // 已用 Basic 仍 401:凭据错误 } a.mode = authModeBasic a.knownBasicOK = true return true } return false } // parseChallengeParams 解析 "realm=\"x\", nonce=\"y\"" 形式的挑战参数(引号内逗号不切分)。 func parseChallengeParams(s string) map[string]string { out := map[string]string{} for _, item := range splitAuthParams(s) { kv := strings.SplitN(item, "=", 2) if len(kv) != 2 { continue } k := strings.ToLower(strings.TrimSpace(kv[0])) v := strings.TrimSpace(kv[1]) if len(v) >= 2 && strings.HasPrefix(v, `"`) && strings.HasSuffix(v, `"`) { v = v[1 : len(v)-1] } out[k] = v } return out } // splitAuthParams 逗号切分但忽略引号内的逗号。 func splitAuthParams(s string) []string { var parts []string var b strings.Builder inQuote := false for i := 0; i < len(s); i++ { c := s[i] switch { case c == '"': inQuote = !inQuote b.WriteByte(c) case c == ',' && !inQuote: if t := strings.TrimSpace(b.String()); t != "" { parts = append(parts, t) } b.Reset() default: b.WriteByte(c) } } if t := strings.TrimSpace(b.String()); t != "" { parts = append(parts, t) } return parts } // algoName 映射哈希函数名。 func algoName(algorithm string) string { switch strings.ToLower(algorithm) { case "sha-256", "sha-256-sess": return "sha256" default: return "md5" } } // hashHex 通用哈希摘要(algo: md5|sha256)。 func hashHex(algo, s string) string { if algo == "sha256" { sum := sha256.Sum256([]byte(s)) return hex.EncodeToString(sum[:]) } sum := md5.Sum([]byte(s)) return hex.EncodeToString(sum[:]) } // escapeDigestValue 转义引号。 func escapeDigestValue(s string) string { return strings.ReplaceAll(s, `"`, `\"`) } // randomHex 生成 n 字节随机 hex。 func randomHex(n int) string { b := make([]byte, n) if _, err := rand.Read(b); err != nil { // crypto/rand 失败极其罕见;退化为全零仍保持协议可用。 for i := range b { b[i] = 0 } } return hex.EncodeToString(b) }