From d0b33ca0c15d8e08275bc4e208cd2fc3bd66392d Mon Sep 17 00:00:00 2001 From: antelle Date: Fri, 20 Nov 2020 14:08:13 +0100 Subject: [PATCH] fix #1523: support for WebDAV servers without Last-Modified header --- app/scripts/const/default-app-settings.js | 1 + app/scripts/locales/base.json | 4 +- app/scripts/storage/impl/storage-webdav.js | 312 ++++++++++-------- .../views/settings/settings-prv-view.js | 10 +- app/templates/settings/settings-prv.hbs | 10 + release-notes.md | 1 + 6 files changed, 195 insertions(+), 143 deletions(-) diff --git a/app/scripts/const/default-app-settings.js b/app/scripts/const/default-app-settings.js index c4dd290a..a6af7112 100644 --- a/app/scripts/const/default-app-settings.js +++ b/app/scripts/const/default-app-settings.js @@ -65,6 +65,7 @@ const DefaultAppSettings = { webdav: true, // enable WebDAV integration webdavSaveMethod: 'move', // how to save files with WebDAV: "move" or "put" + webdavStatReload: false, // WebDAV: reload the file instead of relying on Last-Modified gdrive: true, // enable Google Drive integration gdriveClientId: null, // custom Google Drive client id diff --git a/app/scripts/locales/base.json b/app/scripts/locales/base.json index 5bf6406a..8e0dcbe8 100644 --- a/app/scripts/locales/base.json +++ b/app/scripts/locales/base.json @@ -659,7 +659,9 @@ "webdavSaveMethod": "Save method", "webdavSaveMove": "Upload a temporary file and move", - "webdavSavePut": "Overwrite kdbx file with PUT", + "webdavSavePut": "Overwrite the kdbx file with PUT", + "webdavNoLastModified": "Last-Modified HTTP header is absent", + "webdavStatReload": "Always reload the file instead of relying on Last-Modified HTTP header", "launcherSave": "Save Passwords Database", "launcherFileFilter": "KeePass files", diff --git a/app/scripts/storage/impl/storage-webdav.js b/app/scripts/storage/impl/storage-webdav.js index 3812f988..05ec2d8a 100644 --- a/app/scripts/storage/impl/storage-webdav.js +++ b/app/scripts/storage/impl/storage-webdav.js @@ -1,4 +1,6 @@ +import kdbxweb from 'kdbxweb'; import { StorageBase } from 'storage/storage-base'; +import { Locale } from 'util/locale'; class StorageWebDav extends StorageBase { name = 'webdav'; @@ -48,6 +50,12 @@ class StorageWebDav extends StorageBase { type: 'select', value: this.appSettings.webdavSaveMethod || 'default', options: { default: 'webdavSaveMove', put: 'webdavSavePut' } + }, + { + id: 'webdavStatReload', + title: 'webdavStatReload', + type: 'checkbox', + value: !!this.appSettings.webdavStatReload } ] }; @@ -64,33 +72,67 @@ class StorageWebDav extends StorageBase { method: 'GET', path, user: opts ? opts.user : null, - password: opts ? opts.password : null + password: opts ? opts.password : null, + nostat: this.appSettings.webdavStatReload }, callback ? (err, xhr, stat) => { - callback(err, xhr.response, stat); + if (this.appSettings.webdavStatReload) { + this._calcStatByContent(xhr).then((stat) => + callback(err, xhr.response, stat) + ); + } else { + callback(err, xhr.response, stat); + } } : null ); } stat(path, opts, callback) { - this._request( - { - op: 'Stat', - method: 'HEAD', - path, - user: opts ? opts.user : null, - password: opts ? opts.password : null - }, - callback - ? (err, xhr, stat) => { - callback(err, stat); - } - : null + this._statRequest( + path, + opts, + 'Stat', + callback ? (err, xhr, stat) => callback(err, stat) : null ); } + _statRequest(path, opts, op, callback) { + if (this.appSettings.webdavStatReload) { + this._request( + { + op, + method: 'GET', + path, + user: opts ? opts.user : null, + password: opts ? opts.password : null, + nostat: true + }, + callback + ? (err, xhr) => { + this._calcStatByContent(xhr).then((stat) => callback(err, xhr, stat)); + } + : null + ); + } else { + this._request( + { + op, + method: 'HEAD', + path, + user: opts ? opts.user : null, + password: opts ? opts.password : null + }, + callback + ? (err, xhr, stat) => { + callback(err, xhr, stat); + } + : null + ); + } + } + save(path, opts, data, callback, rev) { const cb = function (err, xhr, stat) { if (callback) { @@ -104,142 +146,113 @@ class StorageWebDav extends StorageBase { user: opts ? opts.user : null, password: opts ? opts.password : null }; - this._request( - { - ...saveOpts, - op: 'Save:stat', - method: 'HEAD' - }, - (err, xhr, stat) => { - let useTmpPath = this.appSettings.webdavSaveMethod !== 'put'; - if (err) { - if (!err.notFound) { - return cb(err); - } else { - this.logger.debug('Save: not found, creating'); - useTmpPath = false; - } - } else if (stat.rev !== rev) { - this.logger.debug('Save error', path, 'rev conflict', stat.rev, rev); - return cb({ revConflict: true }, xhr, stat); - } - if (useTmpPath) { - this._request( - { - ...saveOpts, - op: 'Save:put', - method: 'PUT', - path: tmpPath, - data, - nostat: true - }, - (err) => { - if (err) { - return cb(err); - } - this._request( - { - ...saveOpts, - op: 'Save:stat', - method: 'HEAD' - }, - (err, xhr, stat) => { - if (err) { - this._request({ - ...saveOpts, - op: 'Save:delete', - method: 'DELETE', - path: tmpPath - }); - return cb(err, xhr, stat); - } - if (stat.rev !== rev) { - this.logger.debug( - 'Save error', - path, - 'rev conflict', - stat.rev, - rev - ); - this._request({ - ...saveOpts, - op: 'Save:delete', - method: 'DELETE', - path: tmpPath - }); - return cb({ revConflict: true }, xhr, stat); - } - let movePath = path; - if (movePath.indexOf('://') < 0) { - if (movePath.indexOf('/') === 0) { - movePath = - location.protocol + '//' + location.host + movePath; - } else { - movePath = location.href - .replace(/\?(.*)/, '') - .replace(/[^/]*$/, movePath); - } - } - this._request( - { - ...saveOpts, - op: 'Save:move', - method: 'MOVE', - path: tmpPath, - nostat: true, - headers: { - Destination: encodeURI(movePath), - 'Overwrite': 'T' - } - }, - (err) => { - if (err) { - return cb(err); - } - this._request( - { - ...saveOpts, - op: 'Save:stat', - method: 'HEAD' - }, - (err, xhr, stat) => { - cb(err, xhr, stat); - } - ); - } - ); - } - ); - } - ); + this._statRequest(path, opts, 'Save:stat', (err, xhr, stat) => { + let useTmpPath = this.appSettings.webdavSaveMethod !== 'put'; + if (err) { + if (!err.notFound) { + return cb(err); } else { - this._request( - { - ...saveOpts, - op: 'Save:put', - method: 'PUT', - data, - nostat: true - }, - (err) => { + this.logger.debug('Save: not found, creating'); + useTmpPath = false; + } + } else if (stat.rev !== rev) { + this.logger.debug('Save error', path, 'rev conflict', stat.rev, rev); + return cb({ revConflict: true }, xhr, stat); + } + if (useTmpPath) { + this._request( + { + ...saveOpts, + op: 'Save:put', + method: 'PUT', + path: tmpPath, + data, + nostat: true + }, + (err) => { + if (err) { + return cb(err); + } + this._statRequest(path, opts, 'Save:stat', (err, xhr, stat) => { if (err) { - return cb(err); + this._request({ + ...saveOpts, + op: 'Save:delete', + method: 'DELETE', + path: tmpPath + }); + return cb(err, xhr, stat); + } + if (stat.rev !== rev) { + this.logger.debug( + 'Save error', + path, + 'rev conflict', + stat.rev, + rev + ); + this._request({ + ...saveOpts, + op: 'Save:delete', + method: 'DELETE', + path: tmpPath + }); + return cb({ revConflict: true }, xhr, stat); + } + let movePath = path; + if (movePath.indexOf('://') < 0) { + if (movePath.indexOf('/') === 0) { + movePath = location.protocol + '//' + location.host + movePath; + } else { + movePath = location.href + .replace(/\?(.*)/, '') + .replace(/[^/]*$/, movePath); + } } this._request( { ...saveOpts, - op: 'Save:stat', - method: 'HEAD' + op: 'Save:move', + method: 'MOVE', + path: tmpPath, + nostat: true, + headers: { + Destination: encodeURI(movePath), + 'Overwrite': 'T' + } }, - (err, xhr, stat) => { - cb(err, xhr, stat); + (err) => { + if (err) { + return cb(err); + } + this._statRequest(path, opts, 'Save:stat', (err, xhr, stat) => { + cb(err, xhr, stat); + }); } ); + }); + } + ); + } else { + this._request( + { + ...saveOpts, + op: 'Save:put', + method: 'PUT', + data, + nostat: true + }, + (err) => { + if (err) { + return cb(err); } - ); - } + this._statRequest(path, opts, 'Save:stat', (err, xhr, stat) => { + cb(err, xhr, stat); + }); + } + ); } - ); + }); } fileOptsToStoreOpts(opts, file) { @@ -317,7 +330,7 @@ class StorageWebDav extends StorageBase { this.logger.ts(ts) ); if (callback) { - callback('No Last-Modified header', xhr); + callback(Locale.webdavNoLastModified, xhr); callback = null; } return; @@ -367,6 +380,23 @@ class StorageWebDav extends StorageBase { xhr.send(); } } + + _calcStatByContent(xhr) { + if ( + xhr.status !== 200 || + xhr.responseType !== 'arraybuffer' || + !xhr.response || + !xhr.response.byteLength + ) { + this.logger.debug('Cannot calculate rev by content'); + return null; + } + return kdbxweb.CryptoEngine.sha256(xhr.response).then((hash) => { + const rev = kdbxweb.ByteUtils.bytesToHex(hash).substr(0, 10); + this.logger.debug('Calculated rev by content', `${xhr.response.byteLength} bytes`, rev); + return { rev }; + }); + } } export { StorageWebDav }; diff --git a/app/scripts/views/settings/settings-prv-view.js b/app/scripts/views/settings/settings-prv-view.js index 1fddd997..12d79096 100644 --- a/app/scripts/views/settings/settings-prv-view.js +++ b/app/scripts/views/settings/settings-prv-view.js @@ -7,7 +7,8 @@ class SettingsPrvView extends View { events = { 'change .settings__general-prv-field-sel': 'changeField', - 'input .settings__general-prv-field-txt': 'changeField' + 'input .settings__general-prv-field-txt': 'changeField', + 'change .settings__general-prv-field-check': 'changeCheckbox' }; render() { @@ -29,6 +30,13 @@ class SettingsPrvView extends View { this.render(); } } + + changeCheckbox(e) { + const id = e.target.dataset.id; + const value = !!e.target.checked; + const storage = Storage[this.model.name]; + storage.applySetting(id, value); + } } export { SettingsPrvView }; diff --git a/app/templates/settings/settings-prv.hbs b/app/templates/settings/settings-prv.hbs index 47f8c955..9eadf891 100644 --- a/app/templates/settings/settings-prv.hbs +++ b/app/templates/settings/settings-prv.hbs @@ -14,6 +14,16 @@ {{/each}} + {{else ifeq type 'checkbox'}} + + + {{#if desc}}
{{res desc}}
{{/if}} {{else}} {{#if desc}}
{{res desc}}
{{/if}} diff --git a/release-notes.md b/release-notes.md index cd1ae007..1d474d89 100644 --- a/release-notes.md +++ b/release-notes.md @@ -4,6 +4,7 @@ Release notes `-` fixed a performance issue in searching entries `*` improved the "Show all file" checkbox behavior `+` shortcut to copy OTP +`+` support for WebDAV servers without Last-Modified header `*` switched to Dropbox short-lived access tokens `-` fixed several issues in field editing `-` fix #1561: error during loading configs after reset