@sync-in/server
Version:
The secure, open-source platform for file storage, sharing, collaboration, and sync
580 lines (579 loc) • 25.2 kB
JavaScript
"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