Skip to content

Commit 19013df

Browse files
committed
fix(webdav): Validate filesize via PROPFIND to detect partial downloads
fixes #1870 Signed-off-by: Marcel Klehr <mklehr@gmx.net>
1 parent 7b6f892 commit 19013df

File tree

3 files changed

+121
-2
lines changed

3 files changed

+121
-2
lines changed

_locales/en/messages.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,12 @@
119119
"Error040": {
120120
"message": "E040: Could not search for your file name in your Google Drive"
121121
},
122+
"Error041": {
123+
"message": "E041: Remote bookmarks file size differs from the content that was actually downloaded from the server. This might be a temporary network issue. If this error persists please contact the server administrator."
124+
},
125+
"Error042": {
126+
"message": "E042: Remote bookmarks file size could not be retrieved. It is impossible to verify that the bookmarks file was downloaded in full. If this error persists please contact the server administrator."
127+
},
122128
"LabelWebdavurl": {
123129
"message": "WebDAV URL"
124130
},

src/errors/Error.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,4 +341,20 @@ export class GoogleDriveSearchError extends FloccusError {
341341
this.code = 40
342342
Object.setPrototypeOf(this, GoogleDriveSearchError.prototype)
343343
}
344+
}
345+
346+
export class FileSizeMismatch extends FloccusError {
347+
constructor() {
348+
super('E041: Remote bookmarks file size differs from the content that was actually downloaded from the server. This might be a temporary network issue. If this error persists please contact the server administrator.')
349+
this.code = 41
350+
Object.setPrototypeOf(this, FileSizeMismatch.prototype)
351+
}
352+
}
353+
354+
export class FileSizeUnknown extends FloccusError {
355+
constructor() {
356+
super('E042: Remote bookmarks file size could not be retrieved. It is impossible to verify that the bookmarks file was downloaded in full. If this error persists please contact the server administrator.')
357+
this.code = 42
358+
Object.setPrototypeOf(this, FileSizeUnknown.prototype)
359+
}
344360
}

src/lib/adapters/WebDav.ts

Lines changed: 99 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
HttpError, CancelledSyncError,
1111
LockFileError, MissingPermissionsError,
1212
NetworkError, RedirectError, ResourceLockedError,
13-
SlashError
13+
SlashError, FileSizeMismatch, FileSizeUnknown
1414
} from '../../errors/Error'
1515
import { CapacitorHttp as Http } from '@capacitor/core'
1616
import { Capacitor } from '@capacitor/core'
@@ -211,6 +211,23 @@ export default class WebDavAdapter extends CachingAdapter {
211211

212212
if (response.status === 200) {
213213
let xmlDocText = response.data
214+
let fileSize = null
215+
try {
216+
fileSize = await this.getFileSize(fullUrl)
217+
} catch (e) {
218+
console.warn(e)
219+
Logger.log('Error getting file size: ' + e.message)
220+
}
221+
222+
if (fileSize === null) {
223+
throw new FileSizeUnknown()
224+
}
225+
226+
const byteLength = new TextEncoder().encode(xmlDocText).length
227+
if (fileSize !== byteLength) {
228+
Logger.log('File size mismatch: ' + fileSize + ' != ' + xmlDocText.length)
229+
throw new FileSizeMismatch()
230+
}
214231

215232
if (this.server.passphrase) {
216233
try {
@@ -429,6 +446,86 @@ export default class WebDavAdapter extends CachingAdapter {
429446
}
430447
}
431448

449+
async getFileSize(url) {
450+
if (Capacitor.getPlatform() === 'web') {
451+
return this.getFileSizeWeb(url)
452+
} else {
453+
return this.getFileSizeNative(url)
454+
}
455+
}
456+
457+
async getFileSizeWeb(url): Promise<number|null> {
458+
const authString = Base64.encode(
459+
this.server.username + ':' + this.server.password
460+
)
461+
let res
462+
try {
463+
res = await fetch(url,{
464+
method: 'PROPFIND',
465+
headers: {
466+
Authorization: 'Basic ' + authString
467+
},
468+
cache: 'no-store',
469+
credentials: 'omit',
470+
signal: this.abortSignal,
471+
...(!this.server.allowRedirects && {redirect: 'manual'})
472+
})
473+
} catch (e) {
474+
Logger.log('Error Caught')
475+
Logger.log(e)
476+
if (this.abortSignal.aborted) throw new CancelledSyncError()
477+
throw new NetworkError()
478+
}
479+
if (res.status === 0 && !this.server.allowRedirects) {
480+
throw new RedirectError()
481+
}
482+
if (res.status === 401 || res.status === 403) {
483+
throw new AuthenticationError()
484+
}
485+
if (res.status >= 300 && res.status !== 404) {
486+
throw new HttpError(res.status, 'PROPFIND')
487+
}
488+
489+
const xml = await res.text()
490+
const match = xml.match(/<.*?:?getcontentlength>(.*?)</)
491+
return match ? parseInt(match[1]) : null
492+
}
493+
494+
async getFileSizeNative(url): Promise<number|null> {
495+
let res
496+
const authString = Base64.encode(
497+
this.server.username + ':' + this.server.password
498+
)
499+
500+
try {
501+
res = await Http.request({
502+
url: url,
503+
method: 'PROPFIND',
504+
headers: {
505+
Authorization: 'Basic ' + authString,
506+
Pragma: 'no-cache',
507+
'Cache-Control': 'no-cache'
508+
},
509+
responseType: 'text'
510+
})
511+
} catch (e) {
512+
Logger.log('Error Caught')
513+
Logger.log(e)
514+
throw new NetworkError()
515+
}
516+
517+
if (res.status === 401 || res.status === 403) {
518+
throw new AuthenticationError()
519+
}
520+
if (res.status >= 300 && res.status !== 404) {
521+
throw new HttpError(res.status, 'PROPFIND')
522+
}
523+
524+
const xml = res.data
525+
const match = xml.match(/<.*?:?getcontentlength>(.*?)</)
526+
return match ? parseInt(match[1]) : null
527+
}
528+
432529
async downloadFileWeb(url) {
433530
const authString = Base64.encode(
434531
this.server.username + ':' + this.server.password
@@ -461,7 +558,7 @@ export default class WebDavAdapter extends CachingAdapter {
461558
throw new HttpError(res.status, 'GET')
462559
}
463560

464-
return { status: res.status, data: await res.text(), headers: res.headers }
561+
return { status: res.status, data: await res.text(), headers: Object.fromEntries(res.headers.entries()) }
465562
}
466563

467564
async downloadFileNative(fullURL) {

0 commit comments

Comments
 (0)