diff --git a/lib/ldp-container.js b/lib/ldp-container.js index c99328008..b43f74c59 100644 --- a/lib/ldp-container.js +++ b/lib/ldp-container.js @@ -33,8 +33,8 @@ async function addContainerStats (ldp, reqUri, filename, resourceGraph) { } async function addFile (ldp, resourceGraph, containerUri, reqUri, container, file) { - // Skip .meta and .acl - if (file.endsWith(ldp.suffixMeta) || file.endsWith(ldp.suffixAcl)) { + // Skip .meta and .acl and any dot file + if (ldp.isAuxResource(file) || file.startsWith('.')) { return null } diff --git a/lib/ldp.js b/lib/ldp.js index de8789e6a..66f3216ad 100644 --- a/lib/ldp.js +++ b/lib/ldp.js @@ -448,7 +448,7 @@ class LDP { ({ path, contentType } = await this.resourceMapper.mapUrlToFile({ url: options, searchIndex })) stats = await this.stat(path) } catch (err) { - throw error(404, 'Can\'t find file requested: ' + options) + throw error(err.status || 500, err.message) } // Just return, since resource exists @@ -545,8 +545,8 @@ class LDP { throw error(404, 'The container does not exist') } - // Ensure the container is empty (we ignore .meta and .acl) - if (list.some(file => !file.endsWith(this.suffixMeta) && !file.endsWith(this.suffixAcl))) { + // Ensure the container is empty (we ignore .meta, .acl and dot files) + if (list.some(file => !file.startsWith('.') && !file.endsWith(this.suffixMeta) && !file.endsWith(this.suffixAcl))) { throw error(409, 'Container is not empty') } diff --git a/lib/resource-mapper.js b/lib/resource-mapper.js index f64ff15ff..08b4856dd 100644 --- a/lib/resource-mapper.js +++ b/lib/resource-mapper.js @@ -99,7 +99,14 @@ class ResourceMapper { if (filePath.indexOf('/..') >= 0) { throw new Error('Disallowed /.. segment in URL') } + let filename const isFolder = filePath.endsWith('/') + if (!isFolder) { + filename = filePath.split('/').pop() + if (filename.startsWith('.') && !(filename.endsWith('.acl') || filename.endsWith('.meta'))) { + throw new HTTPError(403, `filename not allowed by server: ${filename}`) + } + } const isIndex = searchIndex && filePath.endsWith('/') // Create the path for a new resource @@ -120,12 +127,11 @@ class ResourceMapper { // Determine the path of an existing file } else { // Read all files in the corresponding folder - const filename = filePath.substr(filePath.lastIndexOf('/') + 1) + filename = filePath.substr(filePath.lastIndexOf('/') + 1) const folder = filePath.substr(0, filePath.length - filename.length) - // Find a file with the same name (minus the dollar extension) let match = '' - if (match === '') { // always true to keep indentation + try { const files = await this._readdir(folder) // Search for files with the same name (disregarding a dollar extension) if (!isFolder) { @@ -134,13 +140,15 @@ class ResourceMapper { } else if (searchIndex && files.includes(this._indexFilename)) { match = this._indexFilename } + } catch (err) { + throw new HTTPError(404, `${filePath} Resource not found`) } // Error if no match was found (unless URL ends with '/', then fall back to the folder) if (match === undefined) { if (isIndex) { match = '' } else { - throw new HTTPError(404, `Resource not found: ${pathname}`) + throw new HTTPError(404, `${pathname} Resource not found`) } } path = `${folder}${match}` diff --git a/test/integration/http-test.js b/test/integration/http-test.js index 4ef3f0941..a82d7843e 100644 --- a/test/integration/http-test.js +++ b/test/integration/http-test.js @@ -335,11 +335,19 @@ describe('HTTP APIs', function () { server.get('/invalidfile.foo') .expect(404, done) }) - it('should return 404 for non-existent container', function (done) { // alain + it('should return 404 for non-existent container', function (done) { server.get('/inexistant/') .expect('Accept-Put', 'text/turtle') .expect(404, done) }) + it('should return 403 for existing dot filename', function (done) { + server.get('/sampleContainer/.tmp') + .expect(403, done) + }) + it('should return 403 for non existing dot filename', function (done) { + server.get('/.foo') + .expect(403, done) + }) it('should return basic container link for directories', function (done) { server.get('/') .expect('Link', /http:\/\/www.w3.org\/ns\/ldp#BasicContainer/) @@ -561,6 +569,18 @@ describe('HTTP APIs', function () { .set('content-type', '') .expect(400, done) }) + it('should fail with 403 for existing dot file', function (done) { + server.put('/sampleContainer/.tmp') + .send(putRequestBody) + .set('content-type', 'text/turtle') + .expect(403, done) + }) + it('should fail with 403 for non existing dot file', function (done) { + server.put('/.foo') + .send(putRequestBody) + .set('content-type', 'text/turtle') + .expect(403, done) + }) it('should create new resource and delete old path if different', function (done) { server.put('/put-resource-1.ttl') .send(putRequestBody) @@ -684,6 +704,7 @@ describe('HTTP APIs', function () { createTestResource('/.acl'), createTestResource('/profile/card'), createTestResource('/delete-test-empty-container/.meta.acl'), + createTestResource('/delete-test-empty-container/.tmp'), createTestResource('/put-resource-1.ttl'), createTestResource('/put-resource-with-acl.ttl'), createTestResource('/put-resource-with-acl.ttl.acl'), diff --git a/test/integration/ldp-test.js b/test/integration/ldp-test.js index e8931951d..41c7424cd 100644 --- a/test/integration/ldp-test.js +++ b/test/integration/ldp-test.js @@ -365,7 +365,7 @@ describe('LDP', function () { }) }) - it('should ldp:contains the same files in dir', () => { + it('should ldp:contains the same files in dir (excluding dot files)', () => { ldp.listContainer(path.join(__dirname, '../resources/sampleContainer/'), 'https://server.tld/resources/sampleContainer/', '', 'server.tld') .then(data => { fs.readdir(path.join(__dirname, '../resources/sampleContainer/'), function (err, expectedFiles) { @@ -385,6 +385,8 @@ describe('LDP', function () { files.sort() expectedFiles.sort() + const firstItem = expectedFiles.shift() + assert.deepEqual(firstItem, '.tmp') assert.deepEqual(files, expectedFiles) }) }) diff --git a/test/integration/patch-test.js b/test/integration/patch-test.js index 53ddb90e3..ff7c4b1bf 100644 --- a/test/integration/patch-test.js +++ b/test/integration/patch-test.js @@ -84,6 +84,16 @@ describe('PATCH through text/n3', () => { result: '@prefix : .\n@prefix tim: .\n\ntim:x tim:y tim:z.\n\n' })) + describe('on a non-existing dot file', describePatch({ + path: '/.ttl', + exists: false, + patch: `<> a solid:InsertDeletePatch; + solid:inserts { . }.` + }, { // expected: + status: 403, + text: 'GlobalDashboard' + })) + describe('on a non-existent JSON-LD file', describePatch({ path: '/new.jsonld', exists: false, diff --git a/test/resources/.ttl b/test/resources/.ttl new file mode 100644 index 000000000..e011fb813 --- /dev/null +++ b/test/resources/.ttl @@ -0,0 +1,13 @@ +@prefix ldp: . +@prefix o: . + + + a o:NetWorth; + o:netWorthOf ; + o:asset + , + ; + o:liability + , + , + . diff --git a/test/resources/sampleContainer/.tmp b/test/resources/sampleContainer/.tmp new file mode 100644 index 000000000..e69de29bb diff --git a/test/unit/resource-mapper-test.js b/test/unit/resource-mapper-test.js index 7217c5091..f76a1f891 100644 --- a/test/unit/resource-mapper-test.js +++ b/test/unit/resource-mapper-test.js @@ -143,12 +143,19 @@ describe('ResourceMapper', () => { // GET/HEAD/POST/DELETE/PATCH base cases + itMapsUrl(mapper, 'a URL of a non-existing folder', + { + url: 'http://localhost/space/foo/' + }, + [/* no files */], + new Error('/space/foo/ Resource not found')) + itMapsUrl(mapper, 'a URL of a non-existing file', { url: 'http://localhost/space/foo.html' }, [/* no files */], - new Error('Resource not found: /space/foo.html')) + new Error('/space/foo.html Resource not found')) itMapsUrl(mapper, 'a URL of an existing file with extension', { @@ -328,6 +335,9 @@ describe('ResourceMapper', () => { { url: 'http://localhost/space/' }, + [ + `${rootPath}space/.tmp` // fs.readdir mock needs one file + ], { path: `${rootPath}space/`, contentType: 'text/turtle' @@ -570,6 +580,13 @@ describe('ResourceMapper', () => { url: 'http://example.org/space/foo.html', contentType: 'text/html' }) + + itMapsUrl(mapper, 'a URL of a non-existing dot file', + { + url: 'http://localhost/space/.foo' + }, + [/* no files */], + new Error('filename not allowed by server: .foo')) }) describe('A ResourceMapper instance for a multi-host setup with a subfolder root URL', () => { @@ -673,6 +690,7 @@ function mapsUrl (it, mapper, label, options, files, expected) { function mockReaddir () { mapper._readdir = async (path) => { expect(path.startsWith(`${rootPath}space/`)).to.equal(true) + if (!files.length) return return files.map(f => f.replace(/.*\//, '')) } }