UNPKG

nephele

Version:

Highly customizable and extensible WebDAV server for Node.js and Express.

350 lines 15.1 kB
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