| 234 | } |
| 235 | |
| 236 | export class WebDavClient { |
| 237 | private baseUrl: string; |
| 238 | private authHeader: string; |
| 239 | private allowInsecure: boolean; |
| 240 | /** |
| 241 | * Flips to true after the first 2xx/207 response. Once true, a 401 is |
| 242 | * treated as a server-side throttle (retry-worthy) rather than a credential |
| 243 | * failure. Reset per WebDavClient instance. |
| 244 | */ |
| 245 | private hadAuthSuccess = false; |
| 246 | |
| 247 | constructor(url: string, username: string, password: string, allowInsecure?: boolean) { |
| 248 | // Normalize: remove control chars/whitespace and trailing slash |
| 249 | this.baseUrl = sanitizeWebDavUrl(url); |
| 250 | // Basic auth header |
| 251 | const credentials = `${username}:${password}`; |
| 252 | // Use UTF-8 safe base64 encoding; btoa is unreliable in React Native/Android. |
| 253 | const encoded = Buffer.from(credentials, "utf8").toString("base64"); |
| 254 | this.authHeader = `Basic ${encoded}`; |
| 255 | console.log("[WebDAV] auth configured", { |
| 256 | username: username.length > 2 ? `${username[0]}***${username[username.length - 1]}` : "***", |
| 257 | passwordLength: password.length, |
| 258 | }); |
| 259 | this.allowInsecure = allowInsecure ?? false; |
| 260 | } |
| 261 | |
| 262 | private getTimeout(method: string, explicitTimeoutMs?: number): number { |
| 263 | if (explicitTimeoutMs !== undefined) return explicitTimeoutMs; |
| 264 | const isTransferOperation = method === "PUT" || method === "GET"; |
| 265 | return isTransferOperation ? TRANSFER_TIMEOUT_MS : DEFAULT_TIMEOUT_MS; |
| 266 | } |
| 267 | |
| 268 | private buildUrl(path: string): string { |
| 269 | const normalizedPath = path.startsWith("/") ? path : `/${path}`; |
| 270 | // Encode path segments but preserve / |
| 271 | const encoded = normalizedPath |
| 272 | .split("/") |
| 273 | .map((segment) => encodeURIComponent(segment)) |
| 274 | .join("/"); |
| 275 | return `${this.baseUrl}${encoded}`; |
| 276 | } |
| 277 | |
| 278 | private getAuthHeaders(): Record<string, string> { |
| 279 | return { Authorization: this.authHeader }; |
| 280 | } |
| 281 | |
| 282 | /** True if the status code indicates a transient, retry-worthy failure. */ |
| 283 | private isTransientStatus(status: number): boolean { |
| 284 | // 401 BEFORE any successful auth = real credential failure, do not retry. |
| 285 | // 401 AFTER a successful response = server is throttling / temporarily |
| 286 | // rejecting valid credentials under burst load (Jianguoyun, some NAS). |
| 287 | if (status === 401 && this.hadAuthSuccess) return true; |
| 288 | if (status === 429) return true; |
| 289 | if (status >= 500 && status < 600) return true; |
| 290 | return false; |
| 291 | } |
| 292 | |
| 293 | private async doFetch( |
nothing calls this directly
no outgoing calls
no test coverage detected