fix #1523: support for WebDAV servers without Last-Modified header

This commit is contained in:
antelle 2020-11-20 14:08:13 +01:00
parent bb44c7cb3e
commit d0b33ca0c1
No known key found for this signature in database
GPG Key ID: 63C9777AAB7C563C
6 changed files with 195 additions and 143 deletions

View File

@ -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

View File

@ -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",

View File

@ -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 };

View File

@ -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 };

View File

@ -14,6 +14,16 @@
{{/each}}
</select>
</div>
{{else ifeq type 'checkbox'}}
<input type="checkbox"
class="input-base settings__general-prv-field settings__input settings__general-prv-field-check"
id="settings__general-prv-field-check-{{id}}"
{{#if value}}checked{{/if}}
value="{{value}}"
data-id="{{id}}"
/>
<label for="settings__general-prv-field-check-{{id}}">{{res title}}</label>
{{#if desc}}<div class="settings__general-prv-field-desc muted-color">{{res desc}}</div>{{/if}}
{{else}}
<label for="settings__general-prv-field-txt-{{id}}">{{res title}}:</label>
{{#if desc}}<div class="settings__general-prv-field-desc muted-color">{{res desc}}</div>{{/if}}

View File

@ -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