UNPKG

@sync-in/server

Version:

The secure, open-source platform for file storage, sharing, collaboration, and sync

580 lines (579 loc) 25.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const _common = require("@nestjs/common"); const _testing = require("@nestjs/testing"); const _functions = require("../../../common/functions"); const _shared = require("../../../common/shared"); const _applicationsconstants = require("../../applications.constants"); const _cache = require("../../files/constants/cache"); const _webdav = require("../constants/webdav"); const _ifheader = /*#__PURE__*/ _interop_require_wildcard(require("../utils/if-header")); const _webdav1 = require("../utils/webdav"); const _webdavprotocolguard = require("./webdav-protocol.guard"); function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function(nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); } function _interop_require_wildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = { __proto__: null }; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for(var key in obj){ if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; } // Keep these mocks to control path transforms in COPY/MOVE tests jest.mock('../../../common/shared', ()=>({ decodeUrl: jest.fn((s)=>s) })); jest.mock('../../../common/functions', ()=>({ urlToPath: jest.fn((s)=>s) })); describe(_webdavprotocolguard.WebDAVProtocolGuard.name, ()=>{ let guard; const makeUser = (hasPerm = true)=>({ havePermission: jest.fn(()=>hasPerm), fullName: 'John Doe', email: 'john@doe.tld' }); const makeRes = ()=>({ headers: jest.fn() }); const baseReq = (method, overrides = {})=>({ method, headers: {}, // undefined body allows to hit the "allowed empty body" branch for PROPFIND/LOCK body: undefined, originalUrl: '/webdav/base/path', protocol: 'http', raw: { httpVersion: '1.1' }, user: makeUser(), ...overrides }); const makeCtx = (req, res)=>({ switchToHttp: ()=>({ getRequest: ()=>req, getResponse: ()=>res }) }); const expectIfHeadersParsed = (req)=>{ expect(Array.isArray(req.dav.ifHeaders)).toBe(true); expect(req.dav.ifHeaders.length).toBeGreaterThan(0); }; beforeEach(async ()=>{ jest.clearAllMocks(); const module = await _testing.Test.createTestingModule({ providers: [ _webdavprotocolguard.WebDAVProtocolGuard ] }).compile(); guard = module.get(_webdavprotocolguard.WebDAVProtocolGuard); }); describe('Permissions', ()=>{ it('allows OPTIONS even without WEBDAV permission (responds headers + 200)', async ()=>{ const req = baseReq(_applicationsconstants.HTTP_METHOD.OPTIONS, { user: makeUser(false) }); const res = makeRes(); const ctx = makeCtx(req, res); await expect(guard.canActivate(ctx)).rejects.toThrow(_common.HttpException); try { await guard.canActivate(ctx); } catch (e) { const ex = e; expect(res.headers).toHaveBeenCalledWith(_webdav.OPTIONS_HEADERS); expect(ex.getStatus()).toBe(_common.HttpStatus.OK); } }); it('forbids non-OPTIONS when the user lacks WEBDAV permission', async ()=>{ const req = baseReq(_applicationsconstants.HTTP_METHOD.GET, { user: makeUser(false) }); const res = makeRes(); const ctx = makeCtx(req, res); await expect(guard.canActivate(ctx)).rejects.toMatchObject({ status: _common.HttpStatus.FORBIDDEN }); }); it('falls through for a method not handled by the switch', async ()=>{ const req = baseReq('VIEW', {}); const res = makeRes(); const ctx = makeCtx(req, res); const spy = jest.spyOn(guard, 'setDAVContext'); const result = await guard.canActivate(ctx); expect(result).toBe(true); expect(spy).toHaveBeenCalled(); spy.mockRestore(); }); }); describe('PROPFIND', ()=>{ it('defaults depth=1 (members) when header is missing, and sets ALLPROP with empty body', async ()=>{ const req = baseReq(_applicationsconstants.HTTP_METHOD.PROPFIND, { headers: {}, body: undefined }); const res = makeRes(); const ctx = makeCtx(req, res); await expect(guard.canActivate(ctx)).resolves.toBe(true); expect(req.dav.url).toBe(req.originalUrl); expect(req.dav.depth).toBe(_webdav.DEPTH.MEMBERS); expect(req.dav.body).toBe(_webdav1.PROPFIND_ALL_PROP); expect(req.dav.propfindMode).toBe(_webdav.PROPSTAT.ALLPROP); expect(req.dav.ifHeaders === undefined || req.dav.ifHeaders.length === 0).toBe(true); }); it.each([ { title: 'depth "infinity" normalized to 1 (members)', depth: _webdav.DEPTH.INFINITY }, { title: 'invalid depth normalized to 1 (members)', depth: 'bad' } ])('depth normalization: $title', async ({ depth })=>{ const req = baseReq(_applicationsconstants.HTTP_METHOD.PROPFIND, { headers: { [_webdav.HEADER.DEPTH]: depth }, body: undefined }); const res = makeRes(); const ctx = makeCtx(req, res); await expect(guard.canActivate(ctx)).resolves.toBe(true); expect(req.dav.depth).toBe(_webdav.DEPTH.MEMBERS); }); it('valid XML body with propname and parses If header', async ()=>{ const req = baseReq(_applicationsconstants.HTTP_METHOD.PROPFIND, { headers: { [_webdav.HEADER.DEPTH]: _webdav.DEPTH.RESOURCE, [_webdav.HEADER.IF]: '(<urn:uuid:abc> ["W/\\"ETag\\""])' }, body: '<propfind xmlns="DAV:"><propname/></propfind>' }); const res = makeRes(); const ctx = makeCtx(req, res); await expect(guard.canActivate(ctx)).resolves.toBe(true); expect(req.dav.depth).toBe(_webdav.DEPTH.RESOURCE); expect(req.dav.propfindMode).toBe(_webdav.PROPSTAT.PROPNAME); expectIfHeadersParsed(req); }); it('invalid propfind mode -> 400', async ()=>{ const req = baseReq(_applicationsconstants.HTTP_METHOD.PROPFIND, { headers: {}, body: '<propfind xmlns="DAV:"><unknown/></propfind>' }); const res = makeRes(); const ctx = makeCtx(req, res); await expect(guard.canActivate(ctx)).rejects.toMatchObject({ status: _common.HttpStatus.BAD_REQUEST }); }); it('invalid XML (with code) -> 400', async ()=>{ const req = baseReq(_applicationsconstants.HTTP_METHOD.PROPFIND, { headers: {}, body: '<bad' }); const res = makeRes(); const ctx = makeCtx(req, res); await expect(guard.canActivate(ctx)).rejects.toMatchObject({ status: _common.HttpStatus.BAD_REQUEST }); }); it('specific XML validation error handled -> 400', async ()=>{ const req = baseReq(_applicationsconstants.HTTP_METHOD.PROPFIND, { headers: {}, body: 'this is not XML at all' }); const res = makeRes(); const ctx = makeCtx(req, res); await expect(guard.canActivate(ctx)).rejects.toHaveProperty('status', _common.HttpStatus.BAD_REQUEST); }); it('skips body when parseBody returns false, still parses If header', async ()=>{ const spyParse = jest.spyOn(guard, 'parseBody').mockReturnValue(false); const req = baseReq(_applicationsconstants.HTTP_METHOD.PROPFIND, { headers: { [_webdav.HEADER.IF]: '(<urn:uuid:abc>)' }, body: undefined }); const res = makeRes(); const ctx = makeCtx(req, res); try { await expect(guard.canActivate(ctx)).resolves.toBe(true); expect(req.dav.body).toBeUndefined(); expect(req.dav.propfindMode).toBeUndefined(); expectIfHeadersParsed(req); } finally{ spyParse.mockRestore(); } }); }); describe('PROPPATCH', ()=>{ it('requires "propertyupdate" in body', async ()=>{ const req = baseReq(_applicationsconstants.HTTP_METHOD.PROPPATCH, { headers: {}, body: '<xml/>' }); const res = makeRes(); const ctx = makeCtx(req, res); await expect(guard.canActivate(ctx)).rejects.toMatchObject({ status: _common.HttpStatus.BAD_REQUEST }); }); it('valid body and parses If header', async ()=>{ const req = baseReq(_applicationsconstants.HTTP_METHOD.PROPPATCH, { headers: { [_webdav.HEADER.IF]: '(<urn:uuid:abc>)', [_webdav.HEADER.DEPTH]: _webdav.DEPTH.RESOURCE }, body: '<propertyupdate xmlns="DAV:"><set/></propertyupdate>' }); const res = makeRes(); const ctx = makeCtx(req, res); await expect(guard.canActivate(ctx)).resolves.toBe(true); expect(req.dav.depth).toBe(_webdav.DEPTH.RESOURCE); expectIfHeadersParsed(req); }); }); describe('LOCK', ()=>{ it('timeout=Infinite -> default timeout, owner from string, depth=resource', async ()=>{ const req = baseReq(_applicationsconstants.HTTP_METHOD.LOCK, { headers: { [_webdav.HEADER.TIMEOUT]: 'Infinite', [_webdav.HEADER.DEPTH]: _webdav.DEPTH.RESOURCE }, body: '<lockinfo xmlns="DAV:"><lockscope><exclusive/></lockscope><owner>Custom Owner</owner></lockinfo>' }); const res = makeRes(); const ctx = makeCtx(req, res); await expect(guard.canActivate(ctx)).resolves.toBe(true); expect(req.dav.lock.timeout).toBe(_cache.CACHE_LOCK_DEFAULT_TTL); expect(req.dav.lock.lockscope).toBe(_webdav.LOCK_SCOPE.EXCLUSIVE); expect(req.dav.lock.owner).toContain('Custom Owner'); expect(req.dav.depth).toBe(_webdav.DEPTH.RESOURCE); }); it('timeout Second-10 + non-string owner -> default WebDAV owner, depth=infinity', async ()=>{ const req = baseReq(_applicationsconstants.HTTP_METHOD.LOCK, { headers: { [_webdav.HEADER.TIMEOUT]: 'Second-10', [_webdav.HEADER.DEPTH]: _webdav.DEPTH.INFINITY }, body: '<lockinfo xmlns="DAV:"><lockscope><shared/></lockscope><owner><href>me</href></owner></lockinfo>' }); const res = makeRes(); const ctx = makeCtx(req, res); await expect(guard.canActivate(ctx)).resolves.toBe(true); expect(req.dav.lock.timeout).toBe(10); expect(req.dav.lock.lockscope).toBe(_webdav.LOCK_SCOPE.SHARED); expect(req.dav.lock.owner).toMatch(/^me/); expect(req.dav.depth).toBe(_webdav.DEPTH.INFINITY); }); it('clamps timeout Second-N to default when N exceeds default', async ()=>{ const big = _cache.CACHE_LOCK_DEFAULT_TTL + 1000; const req = baseReq(_applicationsconstants.HTTP_METHOD.LOCK, { headers: { [_webdav.HEADER.TIMEOUT]: `Second-${big}` }, body: '<lockinfo xmlns="DAV:"><lockscope><exclusive/></lockscope></lockinfo>' }); const res = makeRes(); const ctx = makeCtx(req, res); await expect(guard.canActivate(ctx)).resolves.toBe(true); expect(req.dav.lock.timeout).toBe(_cache.CACHE_LOCK_DEFAULT_TTL); }); it('invalid timeout -> NaN (no fallback), depth=infinity', async ()=>{ const req = baseReq(_applicationsconstants.HTTP_METHOD.LOCK, { headers: { [_webdav.HEADER.TIMEOUT]: 'Bad-Token', [_webdav.HEADER.DEPTH]: 'x' }, body: '<lockinfo xmlns="DAV:"><lockscope><exclusive/></lockscope></lockinfo>' }); const res = makeRes(); const ctx = makeCtx(req, res); await expect(guard.canActivate(ctx)).resolves.toBe(true); expect(Number.isNaN(req.dav.lock.timeout)).toBe(true); expect(req.dav.depth).toBe(_webdav.DEPTH.INFINITY); }); it('handles parseInt exception during timeout parsing', async ()=>{ const req = baseReq(_applicationsconstants.HTTP_METHOD.LOCK, { headers: { [_webdav.HEADER.TIMEOUT]: 'Second-' }, body: '<lockinfo xmlns="DAV:"><lockscope><exclusive/></lockscope></lockinfo>' }); const res = makeRes(); const ctx = makeCtx(req, res); const originalParseInt = global.parseInt; global.parseInt = jest.fn().mockImplementation(()=>{ throw new Error('Forced parseInt error'); }); try { await expect(guard.canActivate(ctx)).resolves.toBe(true); expect(req.dav.lock).toBeDefined(); } finally{ global.parseInt = originalParseInt; } }); it('timeout Infinite + empty IfHeader -> default timeout and ifHeaders undefined', async ()=>{ const spyIf = jest.spyOn(_ifheader, 'parseIfHeader').mockReturnValue([]); const req = baseReq(_applicationsconstants.HTTP_METHOD.LOCK, { headers: { [_webdav.HEADER.TIMEOUT]: 'Infinite', [_webdav.HEADER.IF]: '(<any>)' }, body: '<lockinfo xmlns="DAV:"><lockscope><exclusive/></lockscope></lockinfo>' }); const res = makeRes(); const ctx = makeCtx(req, res); try { await expect(guard.canActivate(ctx)).resolves.toBe(true); expect(req.dav.lock.timeout).toBe(_cache.CACHE_LOCK_DEFAULT_TTL); expect(req.dav.ifHeaders).toBeUndefined(); } finally{ spyIf.mockRestore(); } }); it('missing lockinfo -> 400', async ()=>{ const req = baseReq(_applicationsconstants.HTTP_METHOD.LOCK, { headers: {}, body: '<notlockinfo xmlns="DAV:"/>' }); const res = makeRes(); const ctx = makeCtx(req, res); await expect(guard.canActivate(ctx)).rejects.toMatchObject({ status: _common.HttpStatus.BAD_REQUEST }); }); it('invalid lockscope -> 400', async ()=>{ const req = baseReq(_applicationsconstants.HTTP_METHOD.LOCK, { headers: {}, body: '<lockinfo xmlns="DAV:"><lockscope><invalid/></lockscope></lockinfo>' }); const res = makeRes(); const ctx = makeCtx(req, res); await expect(guard.canActivate(ctx)).rejects.toMatchObject({ status: _common.HttpStatus.BAD_REQUEST }); }); it('handles Object.keys(lockscope) exception with malformed lockscope -> 400', async ()=>{ const req = baseReq(_applicationsconstants.HTTP_METHOD.LOCK, { headers: {}, body: '<lockinfo xmlns="DAV:"><lockscope><exclusive/></lockscope></lockinfo>' }); const res = makeRes(); const ctx = makeCtx(req, res); jest.spyOn(guard, 'parseBody').mockImplementation(()=>{ req.dav.body = { lockinfo: { lockscope: null } }; return true; }); await expect(guard.canActivate(ctx)).rejects.toMatchObject({ status: _common.HttpStatus.BAD_REQUEST, message: 'Invalid or undefined lockscope' }); }); it('refresh (no body) -> dav.depth = null', async ()=>{ const req = baseReq(_applicationsconstants.HTTP_METHOD.LOCK, { headers: { [_webdav.HEADER.DEPTH]: _webdav.DEPTH.RESOURCE }, body: undefined }); const res = makeRes(); const ctx = makeCtx(req, res); await expect(guard.canActivate(ctx)).resolves.toBe(true); expect(req.dav.depth).toBeNull(); }); it('no timeout header -> lock.timeout is undefined', async ()=>{ const req = baseReq(_applicationsconstants.HTTP_METHOD.LOCK, { headers: {}, body: '<lockinfo xmlns="DAV:"><lockscope><exclusive/></lockscope></lockinfo>' }); const res = makeRes(); const ctx = makeCtx(req, res); await expect(guard.canActivate(ctx)).resolves.toBe(true); expect(req.dav.lock).toBeDefined(); expect(req.dav.lock.timeout).toBeUndefined(); }); }); describe('UNLOCK', ()=>{ it('missing lock-token -> 400', async ()=>{ const req = baseReq(_applicationsconstants.HTTP_METHOD.UNLOCK); const res = makeRes(); const ctx = makeCtx(req, res); await expect(guard.canActivate(ctx)).rejects.toMatchObject({ status: _common.HttpStatus.BAD_REQUEST }); }); it('sets token in dav.lock and parses If header', async ()=>{ const req = baseReq(_applicationsconstants.HTTP_METHOD.UNLOCK, { headers: { [_webdav.HEADER.LOCK_TOKEN]: ' <abc> ', [_webdav.HEADER.IF]: '(<urn:uuid:abc>)' } }); const res = makeRes(); const ctx = makeCtx(req, res); await expect(guard.canActivate(ctx)).resolves.toBe(true); expect(req.dav.lock.token).toBe('abc'); expectIfHeadersParsed(req); }); }); describe('PUT / DELETE', ()=>{ it('PUT: depth=0 and parses If header', async ()=>{ const req = baseReq(_applicationsconstants.HTTP_METHOD.PUT, { headers: { [_webdav.HEADER.IF]: '(<urn:uuid:abc>)' } }); const res = makeRes(); const ctx = makeCtx(req, res); await expect(guard.canActivate(ctx)).resolves.toBe(true); expect(req.dav.depth).toBe(_webdav.DEPTH.RESOURCE); expectIfHeadersParsed(req); }); it('DELETE: parses If header', async ()=>{ const req = baseReq(_applicationsconstants.HTTP_METHOD.DELETE, { headers: { [_webdav.HEADER.IF]: '(<urn:uuid:abc>)' } }); const res = makeRes(); const ctx = makeCtx(req, res); await expect(guard.canActivate(ctx)).resolves.toBe(true); expectIfHeadersParsed(req); }); }); describe('MKCOL', ()=>{ it('non-zero content-length -> 415', async ()=>{ const req = baseReq(_applicationsconstants.HTTP_METHOD.MKCOL, { headers: { 'content-length': '3' } }); const res = makeRes(); const ctx = makeCtx(req, res); await expect(guard.canActivate(ctx)).rejects.toMatchObject({ status: _common.HttpStatus.UNSUPPORTED_MEDIA_TYPE }); }); it('zero/absent content-length -> depth=0 and parses If header', async ()=>{ const req = baseReq(_applicationsconstants.HTTP_METHOD.MKCOL, { headers: { 'content-length': '0', [_webdav.HEADER.IF]: '(<urn:uuid:abc>)' } }); const res = makeRes(); const ctx = makeCtx(req, res); await expect(guard.canActivate(ctx)).resolves.toBe(true); expect(req.dav.depth).toBe(_webdav.DEPTH.RESOURCE); expectIfHeadersParsed(req); }); }); describe('COPY / MOVE', ()=>{ it('COPY: missing Destination -> 400', async ()=>{ const req = baseReq(_applicationsconstants.HTTP_METHOD.COPY); const res = makeRes(); const ctx = makeCtx(req, res); await expect(guard.canActivate(ctx)).rejects.toMatchObject({ status: _common.HttpStatus.BAD_REQUEST }); }); it('COPY: invalid Destination base path -> 400', async ()=>{ ; _shared.decodeUrl.mockImplementation((s)=>s); _functions.urlToPath.mockImplementation((s)=>'/not-webdav' + s); const req = baseReq(_applicationsconstants.HTTP_METHOD.COPY, { headers: { [_webdav.HEADER.DESTINATION]: '/wrong' } }); const res = makeRes(); const ctx = makeCtx(req, res); await expect(guard.canActivate(ctx)).rejects.toMatchObject({ status: _common.HttpStatus.BAD_REQUEST }); }); it('COPY: valid Destination -> overwrite=true, move=false, depth=infinity and parses If header', async ()=>{ ; _shared.decodeUrl.mockImplementation((s)=>s); _functions.urlToPath.mockImplementation((_s)=>'/webdav/base/path/target'); const req = baseReq(_applicationsconstants.HTTP_METHOD.COPY, { headers: { [_webdav.HEADER.DESTINATION]: '/webdav/base/path/target', [_webdav.HEADER.IF]: '(<urn:uuid:abc>)' } }); const res = makeRes(); const ctx = makeCtx(req, res); await expect(guard.canActivate(ctx)).resolves.toBe(true); expect(req.dav.depth).toBe(_webdav.DEPTH.INFINITY); expect(req.dav.copyMove).toEqual({ overwrite: true, destination: '/webdav/base/path/target', isMove: false }); expectIfHeadersParsed(req); }); it('MOVE: isMove=true and overwrite=false when OVERWRITE header is "f", respects depth header', async ()=>{ ; _shared.decodeUrl.mockImplementation((s)=>s); _functions.urlToPath.mockImplementation((_s)=>'/webdav/base/path/target2'); const req = baseReq(_applicationsconstants.HTTP_METHOD.MOVE, { headers: { [_webdav.HEADER.DESTINATION]: '/webdav/base/path/target2', [_webdav.HEADER.OVERWRITE]: 'f', [_webdav.HEADER.DEPTH]: _webdav.DEPTH.RESOURCE } }); const res = makeRes(); const ctx = makeCtx(req, res); await expect(guard.canActivate(ctx)).resolves.toBe(true); expect(req.dav.depth).toBe(_webdav.DEPTH.RESOURCE); expect(req.dav.copyMove).toEqual({ overwrite: false, destination: '/webdav/base/path/target2', isMove: true }); }); }); }); //# sourceMappingURL=webdav-protocol.guard.spec.js.map