keeweb/app/scripts/storage/impl/storage-webdav.js

408 lines
14 KiB
JavaScript
Raw Normal View History

2021-05-08 11:38:23 +02:00
import * as kdbxweb from 'kdbxweb';
2019-09-15 14:16:32 +02:00
import { StorageBase } from 'storage/storage-base';
import { Locale } from 'util/locale';
2016-03-12 12:22:35 +01:00
2019-09-18 21:26:43 +02:00
class StorageWebDav extends StorageBase {
name = 'webdav';
icon = 'server';
enabled = true;
uipos = 10;
2016-03-12 12:22:35 +01:00
2019-08-18 10:17:09 +02:00
needShowOpenConfig() {
2016-03-13 17:08:25 +01:00
return true;
2019-09-18 21:26:43 +02:00
}
2016-03-13 17:08:25 +01:00
2019-08-18 10:17:09 +02:00
getOpenConfig() {
2016-03-13 17:45:55 +01:00
return {
fields: [
2020-04-23 18:39:59 +02:00
{
id: 'path',
title: 'openUrl',
desc: 'openUrlDesc',
type: 'text',
required: true,
2020-04-23 19:17:05 +02:00
pattern: '^https://.+'
2020-04-23 18:39:59 +02:00
},
2019-08-16 23:05:39 +02:00
{
id: 'user',
title: 'openUser',
desc: 'openUserDesc',
placeholder: 'openUserPlaceholder',
type: 'text'
},
{
id: 'password',
title: 'openPass',
desc: 'openPassDesc',
placeholder: 'openPassPlaceholder',
type: 'password'
}
2016-03-13 17:45:55 +01:00
]
};
2019-09-18 21:26:43 +02:00
}
2016-03-12 12:22:35 +01:00
2019-08-18 10:17:09 +02:00
getSettingsConfig() {
2017-04-16 21:23:18 +02:00
return {
fields: [
2019-08-16 23:05:39 +02:00
{
id: 'webdavSaveMethod',
title: 'webdavSaveMethod',
type: 'select',
2019-09-17 19:50:42 +02:00
value: this.appSettings.webdavSaveMethod || 'default',
2019-08-16 23:05:39 +02:00
options: { default: 'webdavSaveMove', put: 'webdavSavePut' }
},
{
id: 'webdavStatReload',
title: 'webdavStatReload',
type: 'checkbox',
value: !!this.appSettings.webdavStatReload
2019-08-16 23:05:39 +02:00
}
2017-04-16 21:23:18 +02:00
]
};
2019-09-18 21:26:43 +02:00
}
2017-04-16 21:23:18 +02:00
2019-08-18 10:17:09 +02:00
applySetting(key, value) {
2019-09-17 19:50:42 +02:00
this.appSettings[key] = value;
2019-09-18 21:26:43 +02:00
}
2017-04-16 21:23:18 +02:00
2019-08-18 10:17:09 +02:00
load(path, opts, callback) {
2019-08-16 23:05:39 +02:00
this._request(
{
op: 'Load',
method: 'GET',
2019-08-18 10:17:09 +02:00
path,
2019-08-16 23:05:39 +02:00
user: opts ? opts.user : null,
password: opts ? opts.password : null,
nostat: this.appSettings.webdavStatReload
2019-08-16 23:05:39 +02:00
},
callback
? (err, xhr, stat) => {
if (this.appSettings.webdavStatReload) {
this._calcStatByContent(xhr).then((stat) =>
callback(err, xhr.response, stat)
);
} else {
callback(err, xhr.response, stat);
}
2019-08-16 23:05:39 +02:00
}
: null
);
2019-09-18 21:26:43 +02:00
}
2016-03-12 12:22:35 +01:00
2019-08-18 10:17:09 +02:00
stat(path, opts, callback) {
this._statRequest(
path,
opts,
'Stat',
callback ? (err, xhr, stat) => callback(err, stat) : null
2019-08-16 23:05:39 +02:00
);
2019-09-18 21:26:43 +02:00
}
2016-03-12 12:22:35 +01:00
_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
);
}
}
2019-08-18 10:17:09 +02:00
save(path, opts, data, callback, rev) {
2020-06-01 16:53:51 +02:00
const cb = function (err, xhr, stat) {
2016-03-13 09:34:36 +01:00
if (callback) {
callback(err, stat);
callback = null;
}
};
2020-06-01 16:53:51 +02:00
const tmpPath = path.replace(/[^\/]+$/, (m) => '.' + m) + '.' + Date.now();
2017-01-31 07:50:28 +01:00
const saveOpts = {
2019-08-18 10:17:09 +02:00
path,
2016-03-12 17:49:52 +01:00
user: opts ? opts.user : null,
2016-03-13 09:34:36 +01:00
password: opts ? opts.password : null
2016-03-12 17:49:52 +01:00
};
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.logger.debug('Save: not found, creating');
useTmpPath = false;
2017-11-27 22:11:00 +01:00
}
} 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) => {
2019-08-16 23:05:39 +02:00
if (err) {
this._request({
...saveOpts,
op: 'Save:delete',
method: 'DELETE',
path: tmpPath
});
return cb(err, xhr, stat);
2019-08-16 23:05:39 +02:00
}
if (stat.rev !== rev) {
this.logger.debug(
'Save error',
path,
'rev conflict',
stat.rev,
rev
);
this._request({
2019-10-15 20:02:44 +02:00
...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);
2019-08-16 23:05:39 +02:00
}
2017-04-16 21:23:18 +02:00
}
// prevent double encoding, see #1729
const encodedMovePath = /%[A-Z0-9]{2}/.test(movePath)
? movePath
: encodeURI(movePath);
2020-11-20 13:12:06 +01:00
this._request(
2019-09-17 23:44:17 +02:00
{
2019-10-15 20:02:44 +02:00
...saveOpts,
op: 'Save:move',
method: 'MOVE',
path: tmpPath,
nostat: true,
headers: {
Destination: encodedMovePath,
'Overwrite': 'T'
}
2019-09-17 23:44:17 +02:00
},
(err) => {
if (err) {
return cb(err);
}
this._statRequest(path, opts, 'Save:stat', (err, xhr, stat) => {
cb(err, xhr, stat);
});
2019-08-16 23:05:39 +02:00
}
);
});
}
);
} else {
this._request(
{
...saveOpts,
op: 'Save:put',
method: 'PUT',
data,
nostat: true
},
(err) => {
if (err) {
return cb(err);
2017-04-16 21:23:18 +02:00
}
this._statRequest(path, opts, 'Save:stat', (err, xhr, stat) => {
cb(err, xhr, stat);
});
}
);
2017-04-16 21:23:18 +02:00
}
});
2019-09-18 21:26:43 +02:00
}
2016-03-12 17:49:52 +01:00
2019-08-18 10:17:09 +02:00
fileOptsToStoreOpts(opts, file) {
2019-08-16 23:05:39 +02:00
const result = { user: opts.user, encpass: opts.encpass };
2016-03-19 13:43:50 +01:00
if (opts.password) {
2019-09-17 21:39:06 +02:00
const fileId = file.uuid;
2017-01-31 07:50:28 +01:00
const password = opts.password;
2020-12-30 12:44:48 +01:00
const encpass = this._xorString(password, fileId);
2016-03-19 13:43:50 +01:00
result.encpass = btoa(encpass);
}
return result;
2019-09-18 21:26:43 +02:00
}
2016-03-19 13:43:50 +01:00
2019-08-18 10:17:09 +02:00
storeOptsToFileOpts(opts, file) {
2019-08-16 23:05:39 +02:00
const result = { user: opts.user, password: opts.password };
2016-03-19 13:43:50 +01:00
if (opts.encpass) {
2019-09-17 21:39:06 +02:00
const fileId = file.uuid;
2017-01-31 07:50:28 +01:00
const encpass = atob(opts.encpass);
2020-12-30 12:44:48 +01:00
result.password = this._xorString(encpass, fileId);
}
return result;
}
_xorString(str, another) {
let result = '';
for (let i = 0; i < str.length; i++) {
const strCharCode = str.charCodeAt(i);
2020-12-30 12:52:48 +01:00
const anotherIx = i % another.length;
const anotherCharCode = another.charCodeAt(anotherIx);
2020-12-30 12:44:48 +01:00
const resultCharCode = strCharCode ^ anotherCharCode;
result += String.fromCharCode(resultCharCode);
2016-03-19 13:43:50 +01:00
}
return result;
2019-09-18 21:26:43 +02:00
}
2016-03-19 13:43:50 +01:00
2019-08-18 10:17:09 +02:00
_request(config, callback) {
2016-03-13 09:34:36 +01:00
if (config.rev) {
2020-11-20 13:12:06 +01:00
this.logger.debug(config.op, config.path, config.rev);
2016-03-13 09:34:36 +01:00
} else {
2020-11-20 13:12:06 +01:00
this.logger.debug(config.op, config.path);
2016-03-13 09:34:36 +01:00
}
2020-11-20 13:12:06 +01:00
const ts = this.logger.ts();
2017-01-31 07:50:28 +01:00
const xhr = new XMLHttpRequest();
2016-07-17 13:30:38 +02:00
xhr.addEventListener('load', () => {
2016-03-13 09:34:36 +01:00
if ([200, 201, 204].indexOf(xhr.status) < 0) {
2020-11-20 13:12:06 +01:00
this.logger.debug(
2019-08-18 08:05:38 +02:00
config.op + ' error',
config.path,
xhr.status,
2020-11-20 13:12:06 +01:00
this.logger.ts(ts)
2019-08-18 08:05:38 +02:00
);
2017-01-31 07:50:28 +01:00
let err;
2016-03-12 17:49:52 +01:00
switch (xhr.status) {
case 404:
err = { notFound: true };
break;
case 412:
err = { revConflict: true };
break;
default:
err = 'HTTP status ' + xhr.status;
break;
}
2019-08-16 23:05:39 +02:00
if (callback) {
callback(err, xhr);
callback = null;
}
2016-03-12 17:49:52 +01:00
return;
}
2017-01-31 07:50:28 +01:00
const rev = xhr.getResponseHeader('Last-Modified');
2016-03-13 09:34:36 +01:00
if (!rev && !config.nostat) {
2020-11-20 13:12:06 +01:00
this.logger.debug(
2019-08-18 08:05:38 +02:00
config.op + ' error',
config.path,
'no headers',
2020-11-20 13:12:06 +01:00
this.logger.ts(ts)
2019-08-18 08:05:38 +02:00
);
2019-08-16 23:05:39 +02:00
if (callback) {
callback(Locale.webdavNoLastModified, xhr);
2019-08-16 23:05:39 +02:00
callback = null;
}
2016-03-12 17:49:52 +01:00
return;
}
2019-08-18 08:05:38 +02:00
const completedOpName =
config.op + (config.op.charAt(config.op.length - 1) === 'e' ? 'd' : 'ed');
2020-11-20 13:12:06 +01:00
this.logger.debug(completedOpName, config.path, rev, this.logger.ts(ts));
2019-08-16 23:05:39 +02:00
if (callback) {
2019-08-18 10:17:09 +02:00
callback(null, xhr, rev ? { rev } : null);
2019-08-16 23:05:39 +02:00
callback = null;
}
2016-03-12 17:49:52 +01:00
});
2016-07-17 13:30:38 +02:00
xhr.addEventListener('error', () => {
2020-11-20 13:12:06 +01:00
this.logger.debug(config.op + ' error', config.path, this.logger.ts(ts));
2019-08-16 23:05:39 +02:00
if (callback) {
callback('network error', xhr);
callback = null;
}
2016-03-12 17:49:52 +01:00
});
2016-07-17 13:30:38 +02:00
xhr.addEventListener('abort', () => {
2020-11-20 13:12:06 +01:00
this.logger.debug(config.op + ' error', config.path, 'aborted', this.logger.ts(ts));
2019-08-16 23:05:39 +02:00
if (callback) {
callback('aborted', xhr);
callback = null;
}
2016-03-12 17:49:52 +01:00
});
xhr.open(config.method, config.path);
2016-06-10 19:31:16 +02:00
xhr.responseType = 'arraybuffer';
2016-03-12 17:49:52 +01:00
if (config.user) {
2019-08-18 08:05:38 +02:00
xhr.setRequestHeader(
'Authorization',
'Basic ' + btoa(config.user + ':' + config.password)
);
2016-03-12 17:49:52 +01:00
}
2016-03-13 09:34:36 +01:00
if (config.headers) {
2019-09-17 22:17:40 +02:00
for (const [header, value] of Object.entries(config.headers)) {
2016-03-13 09:34:36 +01:00
xhr.setRequestHeader(header, value);
2019-09-17 22:17:40 +02:00
}
2016-03-12 17:49:52 +01:00
}
if (['GET', 'HEAD'].indexOf(config.method) >= 0) {
xhr.setRequestHeader('Cache-Control', 'no-cache');
}
2016-03-12 17:49:52 +01:00
if (config.data) {
2019-08-16 23:05:39 +02:00
const blob = new Blob([config.data], { type: 'application/octet-stream' });
2016-03-12 17:49:52 +01:00
xhr.send(blob);
} else {
xhr.send();
}
2016-03-12 12:22:35 +01:00
}
_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 };
});
}
2019-09-18 21:26:43 +02:00
}
2016-03-12 12:22:35 +01:00
2019-09-15 14:16:32 +02:00
export { StorageWebDav };