nephele
Version:
Highly customizable and extensible WebDAV server for Node.js and Express.
350 lines • 15.1 kB
JavaScript
import { v4 as uuid } from 'uuid';
import { BadRequestError, LockedError, NotAcceptableError, ResourceNotFoundError, ServiceUnavailableError, UnauthorizedError, } from '../Errors/index.js';
import { catchErrors } from '../catchErrors.js';
import { MultiStatus, Status } from '../MultiStatus.js';
import { Method } from './Method.js';
export class LOCK extends Method {
async run(request, response) {
const { url, encoding } = this.getRequestData(request, response);
if (await this.runPlugins(request, response, 'beginLock', {
method: this,
url,
})) {
return;
}
await this.checkAuthorization(request, response, 'LOCK');
const depth = (request.get('Depth') || 'infinity');
const timeoutHeader = request.get('Timeout') || 'Infinite';
const contentType = request.accepts('application/xml', 'text/xml');
if (!contentType) {
throw new NotAcceptableError('Requested content type is not supported.');
}
let resource;
let newResource = false;
try {
resource = await response.locals.adapter.getResource(url, response.locals.baseUrl);
}
catch (e) {
if (e instanceof ResourceNotFoundError) {
resource = await response.locals.adapter.newResource(url, response.locals.baseUrl);
newResource = true;
}
else {
throw e;
}
}
if ((await resource.isCollection()) && !url.toString().endsWith('/')) {
response.set({
'Content-Location': `${url}/`,
});
}
if (await this.runPlugins(request, response, 'preLock', {
method: this,
resource,
})) {
return;
}
const timeoutRequests = timeoutHeader.split(/,\s+/);
let timeout = Infinity;
for (let curTreq of timeoutRequests) {
let tReqSec;
if (curTreq === 'Infinite') {
tReqSec = Infinity;
}
else if (curTreq.startsWith('Second-')) {
tReqSec = parseInt(curTreq.substring('Second-'.length));
}
else {
tReqSec = NaN;
}
if (isNaN(tReqSec)) {
throw new BadRequestError('Timeout header must contain only valid timeouts.');
}
if (tReqSec * 1000 <= this.opts.maxLockTimeout &&
tReqSec * 1000 >= this.opts.minLockTimeout) {
timeout = tReqSec * 1000;
break;
}
}
if (timeout > this.opts.maxLockTimeout) {
timeout = this.opts.maxLockTimeout;
}
else if (timeout < this.opts.minLockTimeout) {
timeout = this.opts.minLockTimeout;
}
const xmlBody = await this.getBodyXML(request, response);
if (xmlBody == null) {
if (await this.runPlugins(request, response, 'preLockRefresh', {
method: this,
resource,
})) {
return;
}
const lockTokens = this.getRequestLockTockens(request);
if (lockTokens.length !== 1) {
throw new BadRequestError('LOCK method for refreshing a lock must include exactly one lock token in the If header.');
}
const token = lockTokens[0];
const locks = await this.getLocksByUser(request, response, resource, response.locals.user);
const lock = locks.resource.find((lock) => lock.token === token) ||
locks.depthInfinity.find((lock) => lock.token === token);
if (lock == null) {
const multiStatus = new MultiStatus();
let status = new Status(url, 412);
status.setBody({ error: [{ 'lock-token-matches-request-uri': {} }] });
response.locals.errors.push(status);
multiStatus.addStatus(status);
const responseXml = await this.renderXml(multiStatus.render());
response.status(207);
response.set({
'Content-Type': `${contentType}; charset=utf-8`,
});
this.sendBodyContent(response, responseXml, encoding);
return;
}
await this.checkConditionalHeaders(request, response);
if (await this.runPlugins(request, response, 'beforeLockRefresh', {
method: this,
resource,
lock,
})) {
return;
}
lock.date = new Date();
lock.timeout = timeout;
await lock.save();
const currentLocks = await this.getLocks(request, response, resource);
const responseObj = {
prop: {
lockdiscovery: await this.formatLocks(currentLocks.all),
},
};
const responseXml = await this.renderXml(responseObj);
response.status(200);
response.set({
'Content-Type': `${contentType}; charset=utf-8`,
});
this.sendBodyContent(response, responseXml, encoding);
await this.runPlugins(request, response, 'afterLock', {
method: this,
resource,
lock,
});
return;
}
if (!['0', 'infinity'].includes(depth)) {
throw new BadRequestError('Depth header, if present must be one of "0", or "infinity".');
}
const { output: xml, prefixes } = await this.parseXml(xmlBody);
if (xml == null) {
throw new BadRequestError('The given body was not understood by the server.');
}
if (!('lockinfo' in xml)) {
throw new BadRequestError('LOCK methods requires a lockinfo element.');
}
if (!('lockscope' in xml.lockinfo) || !xml.lockinfo.lockscope.length) {
throw new BadRequestError('LOCK methods requires a lockscope element.');
}
const lockscopeXml = xml.lockinfo.lockscope[0];
if (!('locktype' in xml.lockinfo) || !xml.lockinfo.locktype.length) {
throw new BadRequestError('LOCK methods requires a locktype element.');
}
const locktypeXml = xml.lockinfo.locktype[0];
if (!('owner' in xml.lockinfo) ||
!xml.lockinfo.owner.length ||
Object.keys(xml.lockinfo.owner[0]).length === 0) {
throw new BadRequestError('LOCK method requires a filled owner element.');
}
const owner = xml.lockinfo.owner[0];
if (!('write' in locktypeXml)) {
throw new BadRequestError('This server only supports write locks.');
}
if (!('exclusive' in lockscopeXml) && !('shared' in lockscopeXml)) {
throw new BadRequestError('This server only supports exclusive and shared locks.');
}
const scope = 'exclusive' in lockscopeXml ? 'exclusive' : 'shared';
const checkForLockAbove = async () => {
const lockPermission = await this.getLockPermission(request, response, resource, response.locals.user);
if (newResource && lockPermission === 1) {
throw new LockedError('The user does not have permission to create an empty resource in the locked collection.');
}
if (lockPermission === 0) {
throw new LockedError(`The user does not have permission to ${newResource ? 'create' : 'lock'} the locked resource.`);
}
if (lockPermission === 3 && scope === 'exclusive') {
throw new LockedError(`The user does not have permission to ${newResource ? 'create' : 'lock'} the locked resource with an exclusive lock.`);
}
};
await new Promise(async (resolve, reject) => {
let attempt = 0;
const runLockAndProvisionalCheck = async () => {
await checkForLockAbove();
const provisionalLocks = await this.getProvisionalLocks(request, response, resource);
if (provisionalLocks.all.length) {
if (attempt >= 120) {
throw new ServiceUnavailableError('The server is waiting for another lock operation to complete.');
}
await new Promise((resolve) => setTimeout(resolve, 100 + Math.random() * 400));
attempt++;
await runLockAndProvisionalCheck();
}
};
try {
await runLockAndProvisionalCheck();
resolve();
}
catch (e) {
reject(e);
}
});
await this.checkConditionalHeaders(request, response);
if (await this.runPlugins(request, response, 'beforeLockProvisional', {
method: this,
resource,
})) {
return;
}
const multiStatus = new MultiStatus();
response.set({
'Cache-Control': 'private, no-cache',
Date: new Date().toUTCString(),
});
const lock = await resource.createLockForUser(response.locals.user);
lock.token = `urn:uuid:${uuid()}`;
lock.date = new Date();
lock.timeout = 1000 * 60 * 5;
lock.scope = scope;
lock.depth = depth;
lock.owner = owner;
lock.provisional = true;
await lock.save();
try {
await checkForLockAbove();
}
catch (e) {
await lock.delete();
throw e;
}
const checkForPermissionAndLocksBelow = async (resource, firstLevel = true) => {
const resourceUrl = await resource.getCanonicalUrl();
if (!firstLevel &&
(await this.isAdapterRoot(request, response, resourceUrl))) {
const absoluteUrl = new URL(resourceUrl.toString().replace(/\/?$/, () => '/'));
const adapter = await this.getAdapter(request, response, decodeURIComponent(resourceUrl.pathname.substring(request.baseUrl.length)));
resource = await adapter.getResource(absoluteUrl, absoluteUrl);
}
if (!(await resource.adapter.isAuthorized(await resource.getCanonicalUrl(), 'LOCK', resource.baseUrl, response.locals.user))) {
throw new UnauthorizedError('The user is not authorized to lock the resource.');
}
const locks = await this.getCurrentResourceLocks(resource);
if (locks.length &&
(locks.length !== 1 || locks[0].token !== lock.token)) {
if (lock.scope === 'exclusive') {
throw new LockedError('Cannot create a conflicting lock.');
}
for (let checkLock of locks) {
if (checkLock.token === lock.token) {
continue;
}
if (checkLock.scope === 'exclusive') {
throw new LockedError('Cannot create a conflicting lock.');
}
}
}
if ((await resource.isCollection()) &&
((lock.depth === '0' && firstLevel) || lock.depth === 'infinity')) {
const children = await resource.getInternalMembers(response.locals.user);
for (let child of children) {
const run = catchErrors(async () => {
if (!multiStatus.statuses.length) {
await checkForPermissionAndLocksBelow(child, false);
}
}, async (code, message, error) => {
if (code === 500 && error) {
response.locals.debug('Unknown Error: %o', error);
}
const url = await child.getCanonicalUrl();
let status = new Status(url, code);
if (message) {
status.description = message;
}
if (error instanceof LockedError) {
status.setBody({ error: [{ 'no-conflicting-lock': {} }] });
}
response.locals.errors.push(status);
multiStatus.addStatus(status);
});
await run();
}
}
};
const run = catchErrors(async () => {
await checkForPermissionAndLocksBelow(resource);
}, async (code, message, error) => {
if (code === 500 && error) {
response.locals.debug('Unknown Error: %o', error);
}
const url = await resource.getCanonicalUrl();
let status = new Status(url, code);
if (message) {
status.description = message;
}
if (error instanceof LockedError) {
status.setBody({ error: [{ 'no-conflicting-lock': {} }] });
}
multiStatus.addStatus(status);
});
await run();
if (multiStatus.statuses.length) {
await lock.delete();
let status = new Status(url, 424);
multiStatus.addStatus(status);
const responseXml = await this.renderXml(multiStatus.render(), prefixes);
response.status(207);
response.set({
'Content-Type': `${contentType}; charset=utf-8`,
});
this.sendBodyContent(response, responseXml, encoding);
return;
}
if (newResource) {
try {
await resource.create(response.locals.user);
}
catch (e) {
await lock.delete();
throw e;
}
}
if (await this.runPlugins(request, response, 'beforeLock', {
method: this,
resource,
lock,
})) {
return;
}
lock.date = new Date();
lock.timeout = timeout;
lock.provisional = false;
await lock.save();
const currentLocks = await this.getLocks(request, response, resource);
const responseObj = {
prop: {
lockdiscovery: await this.formatLocks(currentLocks.all),
},
};
const responseXml = await this.renderXml(responseObj, prefixes);
response.status(newResource ? 201 : 200);
response.set({
'Lock-Token': `<${lock.token}>`,
'Content-Type': `${contentType}; charset=utf-8`,
});
this.sendBodyContent(response, responseXml, encoding);
await this.runPlugins(request, response, 'afterLock', {
method: this,
resource,
lock,
});
}
}
//# sourceMappingURL=LOCK.js.map