Skip to content

Commit 5c6b82f

Browse files
authored
Merge pull request #1895 from floccusaddon/fix/webdav-check-filesize
fix(webdav): Validate filesize via PROPFIND to detect partial downloads
2 parents 7b6f892 + 1178c82 commit 5c6b82f

File tree

3 files changed

+123
-2
lines changed

3 files changed

+123
-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: 101 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,25 @@ export default class WebDavAdapter extends CachingAdapter {
211211

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

215234
if (this.server.passphrase) {
216235
try {
@@ -429,6 +448,86 @@ export default class WebDavAdapter extends CachingAdapter {
429448
}
430449
}
431450

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

464-
return { status: res.status, data: await res.text(), headers: res.headers }
563+
return { status: res.status, data: await res.text(), headers: Object.fromEntries(res.headers.entries()) }
465564
}
466565

467566
async downloadFileNative(fullURL) {

0 commit comments

Comments
 (0)