diff --git a/lib/handlers/get.js b/lib/handlers/get.js index 27f70973b..df2d4a8f0 100644 --- a/lib/handlers/get.js +++ b/lib/handlers/get.js @@ -14,7 +14,7 @@ const allow = require('./allow') const translate = require('../utils.js').translate const error = require('../http-error') -const RDFs = require('../ldp').RDF_MIME_TYPES +const RDFs = require('../ldp').mimeTypesAsArray() async function handler (req, res, next) { const ldp = req.app.locals.ldp diff --git a/lib/handlers/put.js b/lib/handlers/put.js index af9110ba9..52dba2461 100644 --- a/lib/handlers/put.js +++ b/lib/handlers/put.js @@ -1,15 +1,27 @@ module.exports = handler +const bodyParser = require('body-parser') const debug = require('debug')('solid:put') const getContentType = require('../utils').getContentType +const HTTPError = require('../http-error') +const { stringToStream } = require('../utils') +const LDP = require('../ldp') async function handler (req, res, next) { - const ldp = req.app.locals.ldp debug(req.originalUrl) res.header('MS-Author-Via', 'SPARQL') + const contentType = req.get('content-type') + if (LDP.mimeTypeIsRdf(contentType) && isAclFile(req)) { + return bodyParser.text({ type: () => true })(req, res, () => putAcl(req, res, next)) + } + return putStream(req, res, next) +} + +async function putStream (req, res, next, stream = req) { + const ldp = req.app.locals.ldp try { - await ldp.put(req, req, getContentType(req.headers)) + await ldp.put(req, stream, getContentType(req.headers)) debug('succeded putting the file') res.sendStatus(201) @@ -20,3 +32,19 @@ async function handler (req, res, next) { return next(err) } } + +async function putAcl (req, res, next) { + const ldp = req.app.locals.ldp + const contentType = req.get('content-type') + const requestUri = `${req.protocol}//${req.get('host')}${req.originalUrl}` + if (ldp.isValidRdf(req.body, requestUri, contentType)) { + const stream = stringToStream(req.body) + return putStream(req, res, next, stream) + } + next(new HTTPError(400, 'RDF file contains invalid syntax')) +} + +function isAclFile (req) { + const originalUrlParts = req.originalUrl.split('.') + return originalUrlParts[originalUrlParts.length - 1] === 'acl' +} diff --git a/lib/ldp.js b/lib/ldp.js index 15e05c37d..b5b6d9671 100644 --- a/lib/ldp.js +++ b/lib/ldp.js @@ -17,7 +17,7 @@ const parse = require('./utils').parse const fetch = require('node-fetch') const { promisify } = require('util') -const RDF_MIME_TYPES = [ +const RDF_MIME_TYPES = new Set([ 'text/turtle', // .ttl 'text/n3', // .n3 'text/html', // RDFa @@ -28,7 +28,7 @@ const RDF_MIME_TYPES = [ 'application/rdf+xml', // .rdf 'application/ld+json', // .jsonld 'application/x-turtle' -] +]) class LDP { constructor (argv = {}) { @@ -203,6 +203,17 @@ class LDP { return await this.put(path, stream, contentType) } + isValidRdf (body, requestUri, contentType) { + const resourceGraph = $rdf.graph() + try { + $rdf.parse(body, resourceGraph, requestUri, contentType) + } catch (err) { + debug.ldp('VALIDATE -- Error parsing data: ' + err) + return false + } + return true + } + async put (url, stream, contentType) { // PUT requests not supported on containers. Use POST instead if ((url.url || url).endsWith('/')) { @@ -451,6 +462,13 @@ class LDP { } return ensureNotExists(this, path.join(containerURI, filename)) } + + static mimeTypeIsRdf (mimeType) { + return RDF_MIME_TYPES.has(mimeType) + } + + static mimeTypesAsArray () { + return Array.from(RDF_MIME_TYPES) + } } module.exports = LDP -module.exports.RDF_MIME_TYPES = RDF_MIME_TYPES diff --git a/lib/models/account-template.js b/lib/models/account-template.js index dfedc916e..a645271b6 100644 --- a/lib/models/account-template.js +++ b/lib/models/account-template.js @@ -5,8 +5,8 @@ const mime = require('mime-types') const recursiveRead = require('recursive-readdir') const fsUtils = require('../common/fs-utils') const templateUtils = require('../common/template-utils') +const LDP = require('../ldp') -const RDF_MIME_TYPES = require('../ldp').RDF_MIME_TYPES const TEMPLATE_EXTENSIONS = [ '.acl', '.meta', '.json', '.hbs', '.handlebars' ] const TEMPLATE_FILES = [ 'card' ] @@ -31,7 +31,6 @@ class AccountTemplate { */ constructor (options = {}) { this.substitutions = options.substitutions || {} - this.rdfMimeTypes = options.rdfMimeTypes || RDF_MIME_TYPES this.templateExtensions = options.templateExtensions || TEMPLATE_EXTENSIONS this.templateFiles = options.templateFiles || TEMPLATE_FILES } @@ -136,11 +135,12 @@ class AccountTemplate { * @return {boolean} */ isTemplate (filePath) { - let parsed = path.parse(filePath) + const parsed = path.parse(filePath) - let isRdf = this.rdfMimeTypes.includes(mime.lookup(filePath)) - let isTemplateExtension = this.templateExtensions.includes(parsed.ext) - let isTemplateFile = this.templateFiles.includes(parsed.base) || + const mimeType = mime.lookup(filePath) + const isRdf = LDP.mimeTypeIsRdf(mimeType) + const isTemplateExtension = this.templateExtensions.includes(parsed.ext) + const isTemplateFile = this.templateFiles.includes(parsed.base) || this.templateExtensions.includes(parsed.base) // the '/.acl' case return isRdf || isTemplateExtension || isTemplateFile diff --git a/test/integration/cors-proxy-test.js b/test/integration/cors-proxy-test.js index 9bcc9ee48..c4f643e55 100644 --- a/test/integration/cors-proxy-test.js +++ b/test/integration/cors-proxy-test.js @@ -1,18 +1,14 @@ var assert = require('chai').assert -var supertest = require('supertest') var path = require('path') var nock = require('nock') -var { checkDnsSettings } = require('../utils') - -var ldnode = require('../../index') +var { checkDnsSettings, setupSupertestServer } = require('../utils') describe('CORS Proxy', () => { - var ldp = ldnode({ + var server = setupSupertestServer({ root: path.join(__dirname, '../resources'), corsProxy: '/proxy', webid: false }) - var server = supertest(ldp) before(checkDnsSettings) diff --git a/test/integration/errors-test.js b/test/integration/errors-test.js index 8b73d5e2f..effe9f44d 100644 --- a/test/integration/errors-test.js +++ b/test/integration/errors-test.js @@ -1,30 +1,20 @@ -var supertest = require('supertest') var path = require('path') - -// Helper functions for the FS -// var rm = require('./utils').rm -// var write = require('./utils').write -// var cp = require('./utils').cp -var read = require('./../utils').read - -var ldnode = require('../../index') +const { read, setupSupertestServer } = require('./../utils') describe('Error pages', function () { // LDP with error pages - var errorLdp = ldnode({ + const errorServer = setupSupertestServer({ root: path.join(__dirname, '../resources'), errorPages: path.join(__dirname, '../resources/errorPages'), webid: false }) - var errorServer = supertest(errorLdp) // LDP with no error pages - var noErrorLdp = ldnode({ + const noErrorServer = setupSupertestServer({ root: path.join(__dirname, '../resources'), noErrorPages: true, webid: false }) - var noErrorServer = supertest(noErrorLdp) function defaultErrorPage (filepath, expected) { var handler = function (res) { diff --git a/test/integration/formats-test.js b/test/integration/formats-test.js index 994a415a3..9a6c04790 100644 --- a/test/integration/formats-test.js +++ b/test/integration/formats-test.js @@ -1,15 +1,13 @@ -var supertest = require('supertest') -var ldnode = require('../../index') var path = require('path') const assert = require('chai').assert +const { setupSupertestServer } = require('../utils') describe('formats', function () { - var ldp = ldnode.createServer({ + const server = setupSupertestServer({ root: path.join(__dirname, '../resources'), webid: false }) - var server = supertest(ldp) describe('HTML', function () { it('should return HTML containing "Hello, World!" if Accept is set to text/html', function (done) { server.get('/hello.html') diff --git a/test/integration/http-test.js b/test/integration/http-test.js index 4a2f0a538..f4095ed12 100644 --- a/test/integration/http-test.js +++ b/test/integration/http-test.js @@ -1,21 +1,19 @@ -var supertest = require('supertest') var fs = require('fs') var li = require('li') -var ldnode = require('../../index') var rm = require('./../utils').rm var path = require('path') const rdf = require('rdflib') +const { setupSupertestServer } = require('../utils') var suffixAcl = '.acl' var suffixMeta = '.meta' -var ldpServer = ldnode.createServer({ +var server = setupSupertestServer({ live: true, dataBrowserPath: 'default', root: path.join(__dirname, '../resources'), auth: 'oidc', webid: false }) -var server = supertest(ldpServer) var { assert, expect } = require('chai') /** diff --git a/test/integration/validate-tts.js b/test/integration/validate-tts.js new file mode 100644 index 000000000..1b45ebd92 --- /dev/null +++ b/test/integration/validate-tts.js @@ -0,0 +1,51 @@ +const fs = require('fs') +const path = require('path') +const { setupSupertestServer } = require('../utils') + +const server = setupSupertestServer({ + live: true, + dataBrowserPath: 'default', + root: path.join(__dirname, '../resources'), + auth: 'oidc', + webid: false +}) + +const invalidTurtleBody = fs.readFileSync(path.join(__dirname, '../resources/invalid1.ttl'), { + 'encoding': 'utf8' +}) + +describe('HTTP requests with invalid Turtle syntax', () => { + describe('PUT API', () => { + it('is allowed with invalid TTL files in general', (done) => { + server.put('/invalid1.ttl') + .send(invalidTurtleBody) + .set('content-type', 'text/turtle') + .expect(201, done) + }) + + it('is not allowed with invalid ACL files', (done) => { + server.put('/invalid1.ttl.acl') + .send(invalidTurtleBody) + .set('content-type', 'text/turtle') + .expect(400, done) + }) + }) + + describe('PATCH API', () => { + it('does not support patching of TTL files', (done) => { + server.patch('/patch-1-initial.ttl') + .send(invalidTurtleBody) + .set('content-type', 'text/turtle') + .expect(415, done) + }) + }) + + describe('POST API (multipart)', () => { + it('does not validate files that are posted', (done) => { + server.post('/') + .attach('invalid1', path.join(__dirname, '../resources/invalid1.ttl')) + .attach('invalid2', path.join(__dirname, '../resources/invalid2.ttl')) + .expect(200, done) + }) + }) +}) diff --git a/test/resources/invalid1.ttl b/test/resources/invalid1.ttl new file mode 100644 index 000000000..a2bc6473c --- /dev/null +++ b/test/resources/invalid1.ttl @@ -0,0 +1,13 @@ +@prefix ldp: . +@prefix o: . + + + test o:NetWorth; + o:netWorthOf ; + o:asset + , + ; + o:liability + , + , + . diff --git a/test/resources/invalid2.ttl b/test/resources/invalid2.ttl new file mode 100644 index 000000000..74afdb8bb --- /dev/null +++ b/test/resources/invalid2.ttl @@ -0,0 +1,9 @@ +@prefix txn: . +@prefix srv: . +@prefix log: . +@prefix xsd: . + +txn:123 invalid log:Transaction ; + log:processedBy srv:A ; + log:processedAt "2015-10-16T10:22:23"^^xsd:dateTime ; + log:statusCode 200 . diff --git a/test/utils.js b/test/utils.js index ae181f8cf..5c68a3260 100644 --- a/test/utils.js +++ b/test/utils.js @@ -1,8 +1,10 @@ -var fs = require('fs-extra') -var rimraf = require('rimraf') -var path = require('path') +const fs = require('fs-extra') +const rimraf = require('rimraf') +const path = require('path') const OIDCProvider = require('@solid/oidc-op') const dns = require('dns') +const ldnode = require('../index') +const supertest = require('supertest') const TEST_HOSTS = ['nic.localhost', 'tim.localhost', 'nicola.localhost'] @@ -80,3 +82,14 @@ exports.loadProvider = function loadProvider (configPath) { return provider.initializeKeyChain(config.keys) }) } + +exports.createServer = createServer +function createServer (options) { + return ldnode.createServer(options) +} + +exports.setupSupertestServer = setupSuperServer +function setupSuperServer (options) { + const ldpServer = createServer(options) + return supertest(ldpServer) +}