From 14b51852107983d03b98406ab644132712fabcd3 Mon Sep 17 00:00:00 2001 From: BigBlueHat Date: Mon, 8 May 2017 16:21:59 -0400 Subject: [PATCH 01/17] Ignore node_modules --- .gitignore | 1 + 1 file changed, 1 insertion(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c2658d7 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules/ From b6c5bd9a3111ead4c18b92a244389293018debf1 Mon Sep 17 00:00:00 2001 From: BigBlueHat Date: Mon, 8 May 2017 16:38:48 -0400 Subject: [PATCH 02/17] Ignore build.js and config.json Enforce local building of code--for now. Prevent accidently over sharing credentials. --- .gitignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index c2658d7..96811e8 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,5 @@ node_modules/ + +# project specifics +build.js +config.json From d935e55bdc58ed27bae405a306de3303bdadcbe4 Mon Sep 17 00:00:00 2001 From: BigBlueHat Date: Mon, 8 May 2017 16:40:36 -0400 Subject: [PATCH 03/17] Host out of www/ folder by default --- .gitignore | 1 + config.json.default | 11 +++++++---- index.js | 18 ++++++++++++------ 3 files changed, 20 insertions(+), 10 deletions(-) diff --git a/.gitignore b/.gitignore index 96811e8..984af91 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ node_modules/ # project specifics build.js config.json +www/ diff --git a/config.json.default b/config.json.default index bfa1fdf..cd7ebec 100644 --- a/config.json.default +++ b/config.json.default @@ -3,11 +3,14 @@ "sslKey": "/path/to/privkey.pem", "sslCert": "/path/to/cert.pem", "proxyURL": "https://dokie.li/proxy?uri=", - "rootPath": ".", + "rootPath": "www/", "basePath": "", - "inboxPath": "inbox/", - "queuePath": "queue/", - "annotationPath": "annotation/", + + "annotationPath": "www/annotation/", + "inboxPath": "www/inbox/", + "queuePath": "www/queue/", + "reportsPath": "www/reports/", + "maxPayloadSize": 10000, "maxResourceCount": 10 } diff --git a/index.js b/index.js index c8025c0..0909f75 100644 --- a/index.js +++ b/index.js @@ -152,17 +152,23 @@ function config(configFile){ config['port'] = config.port || 3000; config['scheme'] = (config.sslKey && config.sslCert) ? 'https' : 'http'; config['authority'] = config.scheme + '://' + config.hostname + ':' + config.port; - config['rootPath'] = config.rootPath || ((process.cwd() != __dirname) ? process.cwd() : '.'); + config['rootPath'] = config.rootPath || ((process.cwd() != __dirname) ? process.cwd() : 'www/'); config['basePath'] = config.basePath || ''; - config['inboxPath'] = config.inboxPath || 'inbox/'; - config['queuePath'] = config.queuePath || 'queue/'; - config['annotationPath'] = config.annotationPath || 'annotation/'; - config['reportsPath'] = config.reportsPath || 'reports/'; + + // pre-provided resource endpoints + config['annotationPath'] = config.annotationPath || config['rootPath'] + 'annotation/'; + config['inboxPath'] = config.inboxPath || config['rootPath'] + 'inbox/'; + config['queuePath'] = config.queuePath || config['rootPath'] + 'queue/'; + config['reportsPath'] = config.reportsPath || config['rootPath'] + 'reports/'; + config['maxPayloadSize'] = config.maxPayloadSize || 100000; config['maxResourceCount'] = config.maxResourceCount || 100; config['proxyURL'] = config.proxyURL || 'https://dokie.li/proxy?uri='; - var createDirectories = [config['inboxPath'], config['queuePath'], config['annotationPath'], config['reportsPath']]; + var createDirectories = [ + config['rootPath'], config['annotationPath'], config['inboxPath'], + config['queuePath'], config['reportsPath'] + ]; createDirectories.forEach(function(path){ if(!fs.existsSync(path)){ fs.mkdirSync(path); } }); //console.log(config); From 738b3b4743a380bd7f86bc50e0fd0ad415921ad2 Mon Sep 17 00:00:00 2001 From: BigBlueHat Date: Wed, 10 May 2017 15:28:21 -0400 Subject: [PATCH 04/17] Copy index.html if absent in www/ This provides for bootstrapping a hosting directory with a minimal discovery HTML document. It is copied on the first run of the server, but not on subsequent runs to avoid overwriting changes. Re-bootstrapping simply requires moving the `index.html` from the `rootPath` directory. --- index.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/index.js b/index.js index 0909f75..2674a3c 100644 --- a/index.js +++ b/index.js @@ -171,6 +171,14 @@ function config(configFile){ ]; createDirectories.forEach(function(path){ if(!fs.existsSync(path)){ fs.mkdirSync(path); } }); + // rootPath folder does not contain an index.html file...so we'll copy the + // default one in. + if (!fs.existsSync(config.rootPath + 'index.html') + && fs.existsSync(process.cwd() + '/index.html')) { + fs.createReadStream(process.cwd() + '/index.html') + .pipe(fs.createWriteStream(config.rootPath + 'index.html')); + } + //console.log(config); return config; } From a19a5c84508098d60d286052e47787c93b148fb2 Mon Sep 17 00:00:00 2001 From: BigBlueHat Date: Wed, 10 May 2017 16:12:02 -0400 Subject: [PATCH 05/17] Fix rootPath assumptions to work correctly rootPath is now...root. Other paths are all expected to be served from inside of rootPath (relatively). --- config.json.default | 8 ++++---- index.js | 27 +++++++++++++++++++-------- 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/config.json.default b/config.json.default index cd7ebec..0e1e51f 100644 --- a/config.json.default +++ b/config.json.default @@ -6,10 +6,10 @@ "rootPath": "www/", "basePath": "", - "annotationPath": "www/annotation/", - "inboxPath": "www/inbox/", - "queuePath": "www/queue/", - "reportsPath": "www/reports/", + "annotationPath": "annotation/", + "inboxPath": "inbox/", + "queuePath": "queue/", + "reportsPath": "reports/", "maxPayloadSize": 10000, "maxResourceCount": 10 diff --git a/index.js b/index.js index 2674a3c..aef6137 100644 --- a/index.js +++ b/index.js @@ -156,20 +156,31 @@ function config(configFile){ config['basePath'] = config.basePath || ''; // pre-provided resource endpoints - config['annotationPath'] = config.annotationPath || config['rootPath'] + 'annotation/'; - config['inboxPath'] = config.inboxPath || config['rootPath'] + 'inbox/'; - config['queuePath'] = config.queuePath || config['rootPath'] + 'queue/'; - config['reportsPath'] = config.reportsPath || config['rootPath'] + 'reports/'; + config['annotationPath'] = config.annotationPath || 'annotation/'; + config['inboxPath'] = config.inboxPath || 'inbox/'; + config['queuePath'] = config.queuePath || 'queue/'; + config['reportsPath'] = config.reportsPath || 'reports/'; config['maxPayloadSize'] = config.maxPayloadSize || 100000; config['maxResourceCount'] = config.maxResourceCount || 100; config['proxyURL'] = config.proxyURL || 'https://dokie.li/proxy?uri='; - var createDirectories = [ - config['rootPath'], config['annotationPath'], config['inboxPath'], - config['queuePath'], config['reportsPath'] + function createDir(path) { + if (!fs.existsSync(path)) { + fs.mkdirSync(path); + } + } + + // create the `rootPath` directory + createDir(config['rootPath']); + // ...all others are relative to `rootPath` + var createThese = [ + config['annotationPath'], config['inboxPath'], config['queuePath'], + config['reportsPath'] ]; - createDirectories.forEach(function(path){ if(!fs.existsSync(path)){ fs.mkdirSync(path); } }); + createThese.forEach(function(path) { + createDir(config['rootPath'] + path); + }); // rootPath folder does not contain an index.html file...so we'll copy the // default one in. From 9582f97638c28df783681271e343e3db4628232c Mon Sep 17 00:00:00 2001 From: BigBlueHat Date: Wed, 10 May 2017 16:21:16 -0400 Subject: [PATCH 06/17] Fix requestedPath construction Avoids double // issues --- index.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/index.js b/index.js index aef6137..b800803 100644 --- a/index.js +++ b/index.js @@ -276,7 +276,8 @@ console.log(config); app.use(function(req, res, next) { // module.exports.accept = accept = accepts(req); req.requestedType = req.accepts(availableTypes); - req.requestedPath = config.rootPath + req.originalUrl; + // remove initial `/` from `originalUrl since `rootPath` has one + req.requestedPath = config.rootPath + req.originalUrl.substr(1); // console.log(req); // console.log(res); From e94df7d5faca858bd27ab8e079cf19b24d24c1e9 Mon Sep 17 00:00:00 2001 From: BigBlueHat Date: Thu, 11 May 2017 09:19:12 -0400 Subject: [PATCH 07/17] Make www/ the default when no config.json --- index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.js b/index.js index b800803..661a9ff 100644 --- a/index.js +++ b/index.js @@ -152,7 +152,7 @@ function config(configFile){ config['port'] = config.port || 3000; config['scheme'] = (config.sslKey && config.sslCert) ? 'https' : 'http'; config['authority'] = config.scheme + '://' + config.hostname + ':' + config.port; - config['rootPath'] = config.rootPath || ((process.cwd() != __dirname) ? process.cwd() : 'www/'); + config['rootPath'] = config.rootPath || ((process.cwd() != __dirname) ? process.cwd() + '/www/' : 'www/'); config['basePath'] = config.basePath || ''; // pre-provided resource endpoints From 7077aa1005c5368a062785afd08d8f19ca5203de Mon Sep 17 00:00:00 2001 From: BigBlueHat Date: Thu, 11 May 2017 09:21:22 -0400 Subject: [PATCH 08/17] Move createDir out of config() for wider use --- index.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/index.js b/index.js index 661a9ff..0b01dd9 100644 --- a/index.js +++ b/index.js @@ -144,6 +144,12 @@ function getConfigFile(configFile){ return config; } +function createDir(path) { + if (!fs.existsSync(path)) { + fs.mkdirSync(path); + } +} + function config(configFile){ var config = getConfigFile(configFile); @@ -165,12 +171,6 @@ function config(configFile){ config['maxResourceCount'] = config.maxResourceCount || 100; config['proxyURL'] = config.proxyURL || 'https://dokie.li/proxy?uri='; - function createDir(path) { - if (!fs.existsSync(path)) { - fs.mkdirSync(path); - } - } - // create the `rootPath` directory createDir(config['rootPath']); // ...all others are relative to `rootPath` From 538b29fd9100af842c510baba4ef2ec077226577 Mon Sep 17 00:00:00 2001 From: BigBlueHat Date: Mon, 8 May 2017 16:21:47 -0400 Subject: [PATCH 09/17] Extract htmlEntities into src/utils.js --- index.js | 6 ------ src/utils.js | 3 +++ 2 files changed, 3 insertions(+), 6 deletions(-) create mode 100644 src/utils.js diff --git a/index.js b/index.js index 0b01dd9..4998dab 100644 --- a/index.js +++ b/index.js @@ -1500,10 +1500,6 @@ function decodeString(string) { return decodeURIComponent(string.replace(/\+/g, " ")); } -function htmlEntities(s) { - return String(s).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); -} - function preSafe(s) { return String(s).replace(/\\"/g, '"').replace(/\\r\\n/g, "\n").replace(/\"/g, '"').replace(/&/g, '&').replace(//g, '>').replace(/^\s+|\s+$/g, ''); } @@ -1817,14 +1813,12 @@ getConfigFile, config, init, app, - XMLHttpRequest, SimpleRDF, vocab, prefixes, prefixesRDFa, RDFstore, -htmlEntities, preSafe, discoverInbox, getInboxNotifications, diff --git a/src/utils.js b/src/utils.js new file mode 100644 index 0000000..be46e51 --- /dev/null +++ b/src/utils.js @@ -0,0 +1,3 @@ +exports.htmlEntities = function(s) { + return String(s).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); +} From ba802449b23decf48d9b6cda76883b1fc5e1b520 Mon Sep 17 00:00:00 2001 From: BigBlueHat Date: Mon, 8 May 2017 16:51:06 -0400 Subject: [PATCH 10/17] Move getSerialization to utils.js --- index.js | 53 +++------------------------------------------------- src/utils.js | 49 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 50 deletions(-) diff --git a/index.js b/index.js index 4998dab..791dd6c 100644 --- a/index.js +++ b/index.js @@ -13,6 +13,9 @@ var RdfaParser = require('rdf-parser-rdfa') var RdfXmlParser = require('rdf-parser-rdfxml') var SimpleRDFParse = require('simplerdf-parse') +// local requires +var getSerialization = require('./src/utils.js').getSerialization; + var formats = {parsers: {}} formats.parsers['application/ld+json'] = JsonLdParser formats.parsers['text/turtle'] = N3Parser @@ -774,56 +777,6 @@ function getTarget(req, res, next){ }); } -function getSerialization(data, fromContentType, toContentType, serializeOptions, requestedType) { -// console.log('- - -' + fromContentType + ' ' + toContentType + ' ' + requestedType) - if(fromContentType == 'application/ld+json'){ - try { JSON.parse(data) } - catch(error) { - return Promise.resolve({ - 'fromContentType': fromContentType, - 'toContentType': toContentType, - 'result': 'fail', - 'data': error }); - } - } - - return serializeData(data, fromContentType, toContentType, serializeOptions).then( - function(transformedData){ - var outputData = (fromContentType == toContentType) ? data : transformedData; -// console.log(outputData); - - if(requestedType){ - if(requestedType == toContentType || rdfaTypes.indexOf(requestedType) > -1) { - return { - 'fromContentType': fromContentType, - 'toContentType': toContentType, - 'result': 'pass', - 'data': outputData }; - } - else { -// console.log(' ' + fromContentType + ' ' + toContentType + ' ' + requestedType) - return getSerialization(data, fromContentType, requestedType, serializeOptions, requestedType); - } - } - else { - return { - 'fromContentType': fromContentType, - 'toContentType': toContentType, - 'result': 'pass', - 'data': outputData }; - } - }, - function(error){ - // console.log(error); - return Promise.resolve({ - 'fromContentType': fromContentType, - 'toContentType': toContentType, - 'result': 'fail', - 'data': error }); - }); -} - - function handleResource(req, res, next, options){ options = options || {}; diff --git a/src/utils.js b/src/utils.js index be46e51..805b99a 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,3 +1,52 @@ exports.htmlEntities = function(s) { return String(s).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); } + +exports.getSerialization = function(data, fromContentType, toContentType, serializeOptions, requestedType) { +// console.log('- - -' + fromContentType + ' ' + toContentType + ' ' + requestedType) + if(fromContentType == 'application/ld+json'){ + try { JSON.parse(data) } + catch(error) { + return Promise.resolve({ + 'fromContentType': fromContentType, + 'toContentType': toContentType, + 'result': 'fail', + 'data': error }); + } + } + + return serializeData(data, fromContentType, toContentType, serializeOptions).then( + function(transformedData){ + var outputData = (fromContentType == toContentType) ? data : transformedData; +// console.log(outputData); + + if(requestedType){ + if(requestedType == toContentType || rdfaTypes.indexOf(requestedType) > -1) { + return { + 'fromContentType': fromContentType, + 'toContentType': toContentType, + 'result': 'pass', + 'data': outputData }; + } + else { +// console.log(' ' + fromContentType + ' ' + toContentType + ' ' + requestedType) + return getSerialization(data, fromContentType, requestedType, serializeOptions, requestedType); + } + } + else { + return { + 'fromContentType': fromContentType, + 'toContentType': toContentType, + 'result': 'pass', + 'data': outputData }; + } + }, + function(error){ + // console.log(error); + return Promise.resolve({ + 'fromContentType': fromContentType, + 'toContentType': toContentType, + 'result': 'fail', + 'data': error }); + }); +}; From 52d75e6f19497434a91cd2046066ad03ade71612 Mon Sep 17 00:00:00 2001 From: BigBlueHat Date: Mon, 8 May 2017 23:12:10 -0400 Subject: [PATCH 11/17] Make createServer an express app factory Move to src/server.js --- index.js | 24 +----------------------- src/server.js | 24 ++++++++++++++++++++++++ 2 files changed, 25 insertions(+), 23 deletions(-) create mode 100644 src/server.js diff --git a/index.js b/index.js index 791dd6c..d5a7380 100644 --- a/index.js +++ b/index.js @@ -43,9 +43,6 @@ var minimist = require('minimist'); // var extname = path.extname; var etag = require('etag'); var uuid = require('node-uuid'); -var express = require('express'); -var https = require('https'); -var http = require('http'); var XMLHttpRequest = require('xhr2'); //var accepts = require('accepts'); var contentType = require('content-type'); @@ -114,9 +111,6 @@ var prefixes = { var prefixesRDFa = Object.keys(prefixes).map(function(i){ return i + ': ' + prefixes[i]; }).join(' '); var argv; -var app = express(); - -// app.use(compress()); if(!module.parent) { init(); @@ -197,20 +191,6 @@ function config(configFile){ return config; } -function createServer(config){ - if (config.sslKey && config.sslCert) { - var options = { - key: fs.readFileSync(config.sslKey), - cert: fs.readFileSync(config.sslCert), - requestCert: false - }; - https.createServer(options, app).listen(config.port); - } - else { - http.createServer(app).listen(config.port); - } -} - function init(options){ argv = minimist(process.argv.slice(2)); @@ -221,7 +201,7 @@ function init(options){ config = (options && options.config) ? options.config : config(); console.log(config); - createServer(config); + var app = require('./src/server.js').createServer(config); app.use(function(req, res, next) { res.header('X-Powered-By', mayktsoURI); @@ -1761,11 +1741,9 @@ function resStatus(res, status){ //TODO: clean this up module.exports = { -express, getConfigFile, config, init, -app, XMLHttpRequest, SimpleRDF, vocab, diff --git a/src/server.js b/src/server.js new file mode 100644 index 0000000..3713dfd --- /dev/null +++ b/src/server.js @@ -0,0 +1,24 @@ +var fs = require('fs'); + +var express = require('express'); +var http = require('http'); +var https = require('https'); + +// createServer is an oppinionated expres()-based server factor +exports.createServer = function(config){ + var app = express(); + // app.use(compress()); + + if (config.sslKey && config.sslCert) { + var options = { + key: fs.readFileSync(config.sslKey), + cert: fs.readFileSync(config.sslCert), + requestCert: false + }; + https.createServer(options, app).listen(config.port); + } + else { + http.createServer(app).listen(config.port); + } + return app; +} From e691c058f2ebc3e244fb86c6e85e38045c885457 Mon Sep 17 00:00:00 2001 From: BigBlueHat Date: Wed, 10 May 2017 16:45:51 -0400 Subject: [PATCH 12/17] Move server.js; prep for more server code --- index.js | 5 ++++- src/{server.js => server/index.js} | 0 2 files changed, 4 insertions(+), 1 deletion(-) rename src/{server.js => server/index.js} (100%) diff --git a/index.js b/index.js index d5a7380..669c99d 100644 --- a/index.js +++ b/index.js @@ -48,6 +48,9 @@ var XMLHttpRequest = require('xhr2'); var contentType = require('content-type'); var bodyParser = require('body-parser'); +// local requires +var createServer = require('./src/server/index.js').createServer; + var availableTypes = ['application/ld+json', 'text/turtle', 'application/xhtml+xml', 'text/html']; var rdfaTypes = ['application/xhtml+xml', 'text/html']; var mayktsoURI = 'https://github.com/csarven/mayktso'; @@ -201,7 +204,7 @@ function init(options){ config = (options && options.config) ? options.config : config(); console.log(config); - var app = require('./src/server.js').createServer(config); + var app = createServer(config); app.use(function(req, res, next) { res.header('X-Powered-By', mayktsoURI); diff --git a/src/server.js b/src/server/index.js similarity index 100% rename from src/server.js rename to src/server/index.js From 29128c5dac2fc88939ae17559713545349acc2ef Mon Sep 17 00:00:00 2001 From: BigBlueHat Date: Wed, 10 May 2017 17:10:04 -0400 Subject: [PATCH 13/17] Move CORS headers into their own middleware --- index.js | 10 ---------- src/server/cors-headers.js | 13 +++++++++++++ src/server/index.js | 2 ++ 3 files changed, 15 insertions(+), 10 deletions(-) create mode 100644 src/server/cors-headers.js diff --git a/index.js b/index.js index 669c99d..fa1a0a3 100644 --- a/index.js +++ b/index.js @@ -208,16 +208,6 @@ console.log(config); app.use(function(req, res, next) { res.header('X-Powered-By', mayktsoURI); - res.header("Access-Control-Allow-Credentials", "true"); - res.header("Access-Control-Allow-Methods", "GET, HEAD, OPTIONS, POST, PUT"); - if(req.header('Origin')) { - res.header("Access-Control-Allow-Origin", req.header('Origin')); - } - else { - res.header("Access-Control-Allow-Origin", "*"); - } - res.header("Access-Control-Allow-Headers", "Content-Length, Content-Type, If-None-Match, Link, Location, Origin, Slug, X-Requested-With"); - res.header("Access-Control-Expose-Headers", "Accept-Post, Access-Control-Allow-Headers, Access-Control-Allow-Methods, Access-Control-Allow-Origin, Allow, Content-Length, Content-Type, ETag, Last-Modified, Link, Location, Updates-Via, Vary"); return next(); }); diff --git a/src/server/cors-headers.js b/src/server/cors-headers.js new file mode 100644 index 0000000..98f7d57 --- /dev/null +++ b/src/server/cors-headers.js @@ -0,0 +1,13 @@ +module.exports = function(req, res, next) { + res.header("Access-Control-Allow-Credentials", "true"); + res.header("Access-Control-Allow-Methods", "GET, HEAD, OPTIONS, POST, PUT"); + if(req.header('Origin')) { + res.header("Access-Control-Allow-Origin", req.header('Origin')); + } + else { + res.header("Access-Control-Allow-Origin", "*"); + } + res.header("Access-Control-Allow-Headers", "Content-Length, Content-Type, If-None-Match, Link, Location, Origin, Slug, X-Requested-With"); + res.header("Access-Control-Expose-Headers", "Accept-Post, Access-Control-Allow-Headers, Access-Control-Allow-Methods, Access-Control-Allow-Origin, Allow, Content-Length, Content-Type, ETag, Last-Modified, Link, Location, Updates-Via, Vary"); + return next(); +}; diff --git a/src/server/index.js b/src/server/index.js index 3713dfd..72a163e 100644 --- a/src/server/index.js +++ b/src/server/index.js @@ -9,6 +9,8 @@ exports.createServer = function(config){ var app = express(); // app.use(compress()); + app.use(require('./cors-headers.js')); + if (config.sslKey && config.sslCert) { var options = { key: fs.readFileSync(config.sslKey), From 97b8052867d78ce5c9274c870b83fa22ee8e868f Mon Sep 17 00:00:00 2001 From: BigBlueHat Date: Thu, 11 May 2017 09:31:41 -0400 Subject: [PATCH 14/17] Move cors-headers into middleware --- src/server/index.js | 2 +- src/server/{ => middleware}/cors-headers.js | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename src/server/{ => middleware}/cors-headers.js (100%) diff --git a/src/server/index.js b/src/server/index.js index 72a163e..3f18dfa 100644 --- a/src/server/index.js +++ b/src/server/index.js @@ -9,7 +9,7 @@ exports.createServer = function(config){ var app = express(); // app.use(compress()); - app.use(require('./cors-headers.js')); + app.use(require('./middleware/cors-headers.js')); if (config.sslKey && config.sslCert) { var options = { diff --git a/src/server/cors-headers.js b/src/server/middleware/cors-headers.js similarity index 100% rename from src/server/cors-headers.js rename to src/server/middleware/cors-headers.js From edc4608920a14fed568d356e96a0ad60a3508693 Mon Sep 17 00:00:00 2001 From: BigBlueHat Date: Thu, 11 May 2017 09:35:19 -0400 Subject: [PATCH 15/17] Extract log-request-console middleware --- index.js | 15 --------------- src/server/index.js | 1 + src/server/middleware/log-request-console.js | 14 ++++++++++++++ 3 files changed, 15 insertions(+), 15 deletions(-) create mode 100644 src/server/middleware/log-request-console.js diff --git a/index.js b/index.js index fa1a0a3..cfb302b 100644 --- a/index.js +++ b/index.js @@ -234,21 +234,6 @@ console.log(config); }); app.enable('trust proxy'); - app.use(function(req, res, next){ - require('console-stamp')(console, { - pattern: "yyyy-mm-dd HH:MM:ss.l", - metadata: function () { - return (req.method + ' ' + req.getUrl() + ' ' + req.ips + ''); - }, - colors: { - stamp: "yellow", - label: "white", - metadata: "green" - } - }); - return next(); - }); - app.use(function(req, res, next) { // module.exports.accept = accept = accepts(req); req.requestedType = req.accepts(availableTypes); diff --git a/src/server/index.js b/src/server/index.js index 3f18dfa..384167f 100644 --- a/src/server/index.js +++ b/src/server/index.js @@ -10,6 +10,7 @@ exports.createServer = function(config){ // app.use(compress()); app.use(require('./middleware/cors-headers.js')); + app.use(require('./middleware/log-request-console.js')); if (config.sslKey && config.sslCert) { var options = { diff --git a/src/server/middleware/log-request-console.js b/src/server/middleware/log-request-console.js new file mode 100644 index 0000000..3841a7b --- /dev/null +++ b/src/server/middleware/log-request-console.js @@ -0,0 +1,14 @@ +module.exports = function(req, res, next){ + require('console-stamp')(console, { + pattern: "yyyy-mm-dd HH:MM:ss.l", + metadata: function () { + return (req.method + ' ' + req.getUrl() + ' ' + req.ips + ''); + }, + colors: { + stamp: "yellow", + label: "white", + metadata: "green" + } + }); + return next(); +}; From 064934c0c9fafeba6a64f8d4929ea981abd32864 Mon Sep 17 00:00:00 2001 From: BigBlueHat Date: Thu, 11 May 2017 10:16:23 -0400 Subject: [PATCH 16/17] Extract handleResource + related utils This is a bit heavier refactoring than I like to do in one go. getSerialization() function signature now includes passing in rdfaTypes. Small smell...but we can fix that later. --- index.js | 347 +---------------------- src/server/middleware/handle-resource.js | 279 ++++++++++++++++++ src/utils.js | 64 ++++- 3 files changed, 353 insertions(+), 337 deletions(-) create mode 100644 src/server/middleware/handle-resource.js diff --git a/index.js b/index.js index cfb302b..29ece3f 100644 --- a/index.js +++ b/index.js @@ -14,7 +14,9 @@ var RdfXmlParser = require('rdf-parser-rdfxml') var SimpleRDFParse = require('simplerdf-parse') // local requires -var getSerialization = require('./src/utils.js').getSerialization; +const { getGraph, getGraphFromData, resStatus, serializeData } = require('./src/utils.js'); + +var handleResource = require('./src/server/middleware/handle-resource.js'); var formats = {parsers: {}} formats.parsers['application/ld+json'] = JsonLdParser @@ -52,7 +54,6 @@ var bodyParser = require('body-parser'); var createServer = require('./src/server/index.js').createServer; var availableTypes = ['application/ld+json', 'text/turtle', 'application/xhtml+xml', 'text/html']; -var rdfaTypes = ['application/xhtml+xml', 'text/html']; var mayktsoURI = 'https://github.com/csarven/mayktso'; var vocab = { @@ -261,9 +262,14 @@ console.log(config); var handleRoutes = new RegExp('^(?!/index.html' + oR + ').*$'); app.route(/^\/(index.html)?$/).all(getTarget); - app.route(handleRoutes).all(function(req, res, next){ - handleResource(req, res, next, { jsonld: { profile: 'http://www.w3.org/ns/json-ld#expanded' }}); - }); + app + .route(handleRoutes) + .all(handleResource(config, { + jsonld: { + profile: 'http://www.w3.org/ns/json-ld#expanded' + }, + availableTypes: availableTypes + })); console.log('process.cwd(): ' + process.cwd()); console.log('rootPath: ' + config.rootPath); @@ -735,275 +741,6 @@ function getTarget(req, res, next){ }); } -function handleResource(req, res, next, options){ - options = options || {}; - - switch(req.method){ - case 'GET': case 'HEAD': case 'OPTIONS': - break; - case 'POST': case 'PUT': - return postContainer(req, res, next, options); - break; - default: - res.status(405); - res.set('Allow', 'GET, HEAD, OPTIONS'); - res.end(); - return next(); - break; - } - - if(!req.requestedType){ - resStatus(res, 406); - return next(); - } - - fs.stat(req.requestedPath, function(error, stats) { - if (error) { - if(req.method == 'OPTIONS'){ - res.set('Content-Type', 'text/plain'); - res.set('Content-Length', '0'); - res.set('Vary', 'Origin'); - res.set('Allow', 'GET, HEAD, OPTIONS, PUT, POST'); - res.status(204); - res.end(); - } - else { - res.status(404); - } - return next(); - } - - if (stats.isFile()) { - var isReadable = stats.mode & 4 ? true : false; - if (isReadable) { - fs.readFile(req.requestedPath, 'utf8', function(error, data){ - if (error) { console.log(error); } - - if (req.headers['if-none-match'] && (req.headers['if-none-match'] == etag(data))) { - res.status(304); - res.end(); - return next(); - } - - if(req.requestedPath.startsWith(config.rootPath + '/' + config.queuePath)) { - res.status(200); - res.send(data); - res.end(); - deleteResource(req.requestedPath); - } - - var toContentType = req.requestedType; - var serializeOptions = { 'subjectURI': req.getUrl() }; - - var doSerializations = function(data, serializeOptions){ - var checkSerializations = []; - availableTypes.forEach(function(fromContentType){ - //XXX: toContentType is application/ld+json because we need to see what is serializable since text/html doesn't have a serializer yet. This is not great because we have to rerun the getSerialization in some cases eg resource is Turtle, fromContentType is text/turtle, toContentType is application/ld+json gives a success but the request is text/turtle so we reuse the requestedType in place of toContentType in the second time around. - checkSerializations.push(getSerialization(data, fromContentType, 'application/ld+json', serializeOptions, req.requestedType)); - }); - - return Promise.all(checkSerializations) - .then((serializations) => { -// console.log(serializations); - //If no successful transformation. - if(serializations - .map(function(e){return e.result;}) - .indexOf('pass') < 0){ - resStatus(res, 406); - return next(); - } - else { - var responseSent = false; - serializations.forEach(function(s){ - if(s.result == 'pass' && !responseSent){ - responseSent = true; - //XXX: If success was due to resource being HTML return the data as is, otherwise we can't serialize - var outputData = (req.requestedType == s.fromContentType) ? data : s.data; -// console.log(s); - if(rdfaTypes.indexOf(req.requestedType) > -1){ - if(rdfaTypes.indexOf(s.fromContentType) > -1){ - outputData = data; - } - else { - resStatus(res, 406); - return next(); - } - } - - res.set('Content-Type', req.requestedType +';charset=utf-8'); - res.set('Content-Length', Buffer.byteLength(outputData, 'utf-8')); - res.set('ETag', etag(outputData)); - res.set('Last-Modified', stats.mtime); - res.set('Vary', 'Origin'); - res.set('Allow', 'GET, HEAD, OPTIONS'); - - switch(req.method) { - case 'GET': default: - res.status(200); - res.send(outputData); - break; - case 'HEAD': - res.status(200); - res.send(); - break; - case 'OPTIONS': - res.status(204); - break; - } - - res.end(); - return next(); - } - }); - } - }) - .catch((error) => { - console.log('--- catch: `return Promise.all(checkSerializations)` '); - console.log(error); - res.status(500); - res.end(); - return next(); - }); - } - - doSerializations(data, serializeOptions); - }); - } - else { - res.status(403); - return next(); - } - } - else if(stats.isDirectory()) { - fs.readdir(req.requestedPath, function(error, files){ - if(error) { - console.log("Can't readdir: " + req.requestedPath); //throw err; - } - - var baseURL = req.getUrl().endsWith('/') ? req.getUrl() : req.getUrl() + '/'; - - var profile = 'http://www.w3.org/ns/json-ld#expanded'; - var data, nsLDP = ''; - if(typeof options !== 'undefined' && 'jsonld' in options && 'profile' in options.jsonld){ - switch(options.jsonld.profile){ - default: - profile = 'http://www.w3.org/ns/json-ld#expanded'; - nsLDP = 'http://www.w3.org/ns/ldp#'; - break; - case 'http://www.w3.org/ns/json-ld#compacted': - profile = 'http://www.w3.org/ns/json-ld#compacted'; - break; - } - } - - var contains = []; - for (var i = 0; i < files.length; i++) { - var file = files[i]; - contains.push({ - "@id": baseURL + file, - "@type": [ nsLDP + 'Resource', nsLDP + 'RDFSource' ] - }); - } - - var data = {}; - if(profile == 'http://www.w3.org/ns/json-ld#compacted'){ - data["@context"] = 'http://www.w3.org/ns/ldp'; - } - data = Object.assign(data, { - "@id": baseURL, - "@type": [ nsLDP+'Resource', nsLDP+'RDFSource', nsLDP+'Container', nsLDP+'BasicContainer' ] - }); - - if(contains.length > 0) { - data[nsLDP+'contains'] = contains; - } - - if(profile == 'http://www.w3.org/ns/json-ld#expanded'){ - data = [data]; - } - - data = JSON.stringify(data) + "\n"; - - var respond = function() { - return new Promise(function(resolve, reject) { - if(req.method == 'OPTIONS' || req.requestedType == 'application/ld+json') { - return resolve(data); - } - else { - var fromContentType = 'application/ld+json'; - var toContentType = req.requestedType; - var serializeOptions = { 'subjectURI': req.getUrl() }; - - if(rdfaTypes.indexOf(toContentType) > -1){ - return reject({'toContentType': 'text/html'}); - } - else { - //TODO: the resolve/reject should happen at a lower-level. - return serializeData(data, fromContentType, toContentType, options).then( - function(i) { resolve(i); }, - function(j) { reject(j); } - ); - } - } - }); - }; - - respond().then( - function(data) { - if (req.headers['if-none-match'] && (req.headers['if-none-match'] == etag(data))) { - res.status(304); - res.end(); - return next(); - } - - parameterProfile = ''; - if(req.requestedType == 'application/ld+json') { - parameterProfile = ';profile="'+profile+'"'; - } - - res.set('Link', '; rel="type", ; rel="type", ; rel="type", ; rel="type"'); - res.set('Content-Type', req.requestedType + ';charset=utf-8' + parameterProfile); - res.set('Content-Length', Buffer.byteLength(data, 'utf-8')); - res.set('ETag', etag(data)); - res.set('Last-Modified', stats.mtime); - res.set('Vary', 'Origin'); - res.set('Accept-Post', 'text/html, application/xhtml+xml, application/ld+json, text/turtle'); - res.set('Allow', 'GET, HEAD, OPTIONS, POST'); - - switch(req.method) { - case 'GET': default: - res.status(200); - res.send(data); - break; - case 'HEAD': - res.status(200); - res.send(); - break; - case 'OPTIONS': - res.status(204); - res.send(); - break; - } - res.end(); - return next(); - }, - function(reason){ - if('toContentType' in reason && reason.toContentType == 'text/html'){ - resStatus(res, 406); - } - else { - res.status(500); - res.end(); - } - return next(); - } - ); - }); - } - return; - }); -} - function postContainer(req, res, next, options){ options = options || {}; options['fileNameSuffix'] = ('fileNameSuffix' in options) ? encodeURIComponent(options['fileNameSuffix']) : ''; @@ -1316,47 +1053,6 @@ function formatToMimeType(format){ } } -//From https://github.com/linkeddata/dokieli/scripts/do.js -function getGraphFromData(data, options) { - options = options || {}; - if (!('contentType' in options)) { - options['contentType'] = 'text/turtle'; - } - if (!('subjectURI' in options)) { - options['subjectURI'] = '_:dokieli'; - } - - return SimpleRDF.parse(data, options['contentType'], options['subjectURI']); -} - -function getGraph(url) { - return SimpleRDF(vocab, url, null, RDFstore).get(); -} - -function serializeGraph(g, options) { - options = options || {}; - if (!('contentType' in options)) { - options['contentType'] = 'text/turtle'; - } - - return RDFstore.serializers[options.contentType].serialize(g._graph); -} - -function serializeData(data, fromContentType, toContentType, options) { - var o = { - 'contentType': fromContentType, - 'subjectURI': options.subjectURI - }; - return getGraphFromData(data, o).then( - function(g) { - return serializeGraph(g, { 'contentType': toContentType }); - }, - function(reason) { - return Promise.reject(reason); - } - ); -} - //https://github.com/solid/solid.js/blob/master/lib/util/web-util.js function parseLinkHeader(link) { if (!link) { @@ -1702,21 +1398,6 @@ function getInboxNotifications(data, options) { ); } -function resStatus(res, status){ - res.status(status); - switch(status){ - default: - break; - case 406: - var data = "HTTP 406: Accept type not acceptable. See also https://tools.ietf.org/html/rfc7231#section-6.5.6\n"; - break; - } - if (typeof data !== 'undefined'){ - res.send(data); - } - res.end(); -} - //TODO: clean this up module.exports = { getConfigFile, @@ -1739,12 +1420,6 @@ postResource, putResource, parseLinkHeader, parseProfileLinkRelation, -getGraph, -getGraphFromData, -serializeData, getBaseURL, getExternalBaseURL, -handleResource, -getSerialization, -resStatus, } diff --git a/src/server/middleware/handle-resource.js b/src/server/middleware/handle-resource.js new file mode 100644 index 0000000..95c3290 --- /dev/null +++ b/src/server/middleware/handle-resource.js @@ -0,0 +1,279 @@ +const fs = require('fs'); + +const etag = require('etag'); + +let { getSerialization, resStatus, serializeData } = require('../../utils.js'); + +const rdfaTypes = ['application/xhtml+xml', 'text/html']; + +module.exports = function(config, options) { + options = options || {}; + + return function (req, res, next) { + + switch(req.method){ + case 'GET': case 'HEAD': case 'OPTIONS': + break; + case 'POST': case 'PUT': + return postContainer(req, res, next, options); + break; + default: + res.status(405); + res.set('Allow', 'GET, HEAD, OPTIONS'); + res.end(); + return next(); + break; + } + + if(!req.requestedType){ + resStatus(res, 406); + return next(); + } + + fs.stat(req.requestedPath, function(error, stats) { + if (error) { + if(req.method == 'OPTIONS'){ + res.set('Content-Type', 'text/plain'); + res.set('Content-Length', '0'); + res.set('Vary', 'Origin'); + res.set('Allow', 'GET, HEAD, OPTIONS, PUT, POST'); + res.status(204); + res.end(); + } + else { + res.status(404); + } + return next(); + } + + if (stats.isFile()) { + var isReadable = stats.mode & 4 ? true : false; + if (isReadable) { + fs.readFile(req.requestedPath, 'utf8', function(error, data){ + if (error) { console.log(error); } + + if (req.headers['if-none-match'] && (req.headers['if-none-match'] == etag(data))) { + res.status(304); + res.end(); + return next(); + } + + if(req.requestedPath.startsWith(config.rootPath + '/' + config.queuePath)) { + res.status(200); + res.send(data); + res.end(); + deleteResource(req.requestedPath); + } + + var toContentType = req.requestedType; + var serializeOptions = { 'subjectURI': req.getUrl() }; + + var doSerializations = function(data, serializeOptions){ + var checkSerializations = []; + options.availableTypes.forEach(function(fromContentType){ + //XXX: toContentType is application/ld+json because we need to see what is serializable since text/html doesn't have a serializer yet. This is not great because we have to rerun the getSerialization in some cases eg resource is Turtle, fromContentType is text/turtle, toContentType is application/ld+json gives a success but the request is text/turtle so we reuse the requestedType in place of toContentType in the second time around. + checkSerializations.push(getSerialization(data, fromContentType, 'application/ld+json', serializeOptions, req.requestedType, rdfaTypes)); + }); + + return Promise.all(checkSerializations) + .then((serializations) => { + // console.log(serializations); + //If no successful transformation. + if(serializations + .map(function(e){return e.result;}) + .indexOf('pass') < 0){ + resStatus(res, 406); + return next(); + } + else { + var responseSent = false; + serializations.forEach(function(s){ + if(s.result == 'pass' && !responseSent){ + responseSent = true; + //XXX: If success was due to resource being HTML return the data as is, otherwise we can't serialize + var outputData = (req.requestedType == s.fromContentType) ? data : s.data; + // console.log(s); + if(rdfaTypes.indexOf(req.requestedType) > -1){ + if(rdfaTypes.indexOf(s.fromContentType) > -1){ + outputData = data; + } + else { + resStatus(res, 406); + return next(); + } + } + + res.set('Content-Type', req.requestedType +';charset=utf-8'); + res.set('Content-Length', Buffer.byteLength(outputData, 'utf-8')); + res.set('ETag', etag(outputData)); + res.set('Last-Modified', stats.mtime); + res.set('Vary', 'Origin'); + res.set('Allow', 'GET, HEAD, OPTIONS'); + + switch(req.method) { + case 'GET': default: + res.status(200); + res.send(outputData); + break; + case 'HEAD': + res.status(200); + res.send(); + break; + case 'OPTIONS': + res.status(204); + break; + } + + res.end(); + return next(); + } + }); + } + }) + .catch((error) => { + console.log('--- catch: `return Promise.all(checkSerializations)` '); + console.log(error); + res.status(500); + res.end(); + return next(); + }); + } + + doSerializations(data, serializeOptions); + }); + } + else { + res.status(403); + return next(); + } + } + else if(stats.isDirectory()) { + fs.readdir(req.requestedPath, function(error, files){ + if(error) { + console.log("Can't readdir: " + req.requestedPath); //throw err; + } + + var baseURL = req.getUrl().endsWith('/') ? req.getUrl() : req.getUrl() + '/'; + + var profile = 'http://www.w3.org/ns/json-ld#expanded'; + var data, nsLDP = ''; + if(typeof options !== 'undefined' && 'jsonld' in options && 'profile' in options.jsonld){ + switch(options.jsonld.profile){ + default: + profile = 'http://www.w3.org/ns/json-ld#expanded'; + nsLDP = 'http://www.w3.org/ns/ldp#'; + break; + case 'http://www.w3.org/ns/json-ld#compacted': + profile = 'http://www.w3.org/ns/json-ld#compacted'; + break; + } + } + + var contains = []; + for (var i = 0; i < files.length; i++) { + var file = files[i]; + contains.push({ + "@id": baseURL + file, + "@type": [ nsLDP + 'Resource', nsLDP + 'RDFSource' ] + }); + } + + var data = {}; + if(profile == 'http://www.w3.org/ns/json-ld#compacted'){ + data["@context"] = 'http://www.w3.org/ns/ldp'; + } + data = Object.assign(data, { + "@id": baseURL, + "@type": [ nsLDP+'Resource', nsLDP+'RDFSource', nsLDP+'Container', nsLDP+'BasicContainer' ] + }); + + if(contains.length > 0) { + data[nsLDP+'contains'] = contains; + } + + if(profile == 'http://www.w3.org/ns/json-ld#expanded'){ + data = [data]; + } + + data = JSON.stringify(data) + "\n"; + + var respond = function() { + return new Promise(function(resolve, reject) { + if(req.method == 'OPTIONS' || req.requestedType == 'application/ld+json') { + return resolve(data); + } + else { + var fromContentType = 'application/ld+json'; + var toContentType = req.requestedType; + var serializeOptions = { 'subjectURI': req.getUrl() }; + + if(rdfaTypes.indexOf(toContentType) > -1){ + return reject({'toContentType': 'text/html'}); + } + else { + //TODO: the resolve/reject should happen at a lower-level. + return serializeData(data, fromContentType, toContentType, options).then( + function(i) { resolve(i); }, + function(j) { reject(j); } + ); + } + } + }); + }; + + respond().then( + function(data) { + if (req.headers['if-none-match'] && (req.headers['if-none-match'] == etag(data))) { + res.status(304); + res.end(); + return next(); + } + + parameterProfile = ''; + if(req.requestedType == 'application/ld+json') { + parameterProfile = ';profile="'+profile+'"'; + } + + res.set('Link', '; rel="type", ; rel="type", ; rel="type", ; rel="type"'); + res.set('Content-Type', req.requestedType + ';charset=utf-8' + parameterProfile); + res.set('Content-Length', Buffer.byteLength(data, 'utf-8')); + res.set('ETag', etag(data)); + res.set('Last-Modified', stats.mtime); + res.set('Vary', 'Origin'); + res.set('Accept-Post', 'text/html, application/xhtml+xml, application/ld+json, text/turtle'); + res.set('Allow', 'GET, HEAD, OPTIONS, POST'); + + switch(req.method) { + case 'GET': default: + res.status(200); + res.send(data); + break; + case 'HEAD': + res.status(200); + res.send(); + break; + case 'OPTIONS': + res.status(204); + res.send(); + break; + } + res.end(); + return next(); + }, + function(reason){ + if('toContentType' in reason && reason.toContentType == 'text/html'){ + resStatus(res, 406); + } + else { + res.status(500); + res.end(); + } + return next(); + } + ); + }); + } + return; + }); + }; +}; diff --git a/src/utils.js b/src/utils.js index 805b99a..565bb5e 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,8 +1,55 @@ +const SimpleRDF = require('simplerdf'); + exports.htmlEntities = function(s) { return String(s).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); } -exports.getSerialization = function(data, fromContentType, toContentType, serializeOptions, requestedType) { +//From https://github.com/linkeddata/dokieli/scripts/do.js +function getGraphFromData(data, options) { + options = options || {}; + if (!('contentType' in options)) { + options['contentType'] = 'text/turtle'; + } + if (!('subjectURI' in options)) { + options['subjectURI'] = '_:dokieli'; + } + + return SimpleRDF.parse(data, options['contentType'], options['subjectURI']); +} +exports.getGraphFromData = getGraphFromData; + +function getGraph(url) { + return SimpleRDF(vocab, url, null, RDFstore).get(); +} +exports.getGraph = getGraph; + +function serializeGraph(g, options) { + options = options || {}; + if (!('contentType' in options)) { + options['contentType'] = 'text/turtle'; + } + + return RDFstore.serializers[options.contentType].serialize(g._graph); +} +exports.serializeGraph = serializeGraph; + +function serializeData(data, fromContentType, toContentType, options) { + var o = { + 'contentType': fromContentType, + 'subjectURI': options.subjectURI + }; + return getGraphFromData(data, o).then( + function(g) { + return serializeGraph(g, { 'contentType': toContentType }); + }, + function(reason) { + return Promise.reject(reason); + } + ); +}; +exports.serializeData = serializeData; + +exports.getSerialization = function(data, fromContentType, toContentType, serializeOptions, requestedType, rdfTypes) { // console.log('- - -' + fromContentType + ' ' + toContentType + ' ' + requestedType) if(fromContentType == 'application/ld+json'){ try { JSON.parse(data) } @@ -50,3 +97,18 @@ exports.getSerialization = function(data, fromContentType, toContentType, serial 'data': error }); }); }; + +exports.resStatus = function(res, status) { + res.status(status); + switch(status){ + default: + break; + case 406: + var data = "HTTP 406: Accept type not acceptable. See also https://tools.ietf.org/html/rfc7231#section-6.5.6\n"; + break; + } + if (typeof data !== 'undefined'){ + res.send(data); + } + res.end(); +}; From c614bd35ff1c189bf0104e99fc2653698f2933f5 Mon Sep 17 00:00:00 2001 From: BigBlueHat Date: Wed, 31 May 2017 10:48:09 -0400 Subject: [PATCH 17/17] Make app var global and export it Mostly for future use by other projects --- index.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/index.js b/index.js index 29ece3f..8a56ded 100644 --- a/index.js +++ b/index.js @@ -18,6 +18,8 @@ const { getGraph, getGraphFromData, resStatus, serializeData } = require('./src/ var handleResource = require('./src/server/middleware/handle-resource.js'); +var app; + var formats = {parsers: {}} formats.parsers['application/ld+json'] = JsonLdParser formats.parsers['text/turtle'] = N3Parser @@ -205,7 +207,8 @@ function init(options){ config = (options && options.config) ? options.config : config(); console.log(config); - var app = createServer(config); + // app is a global...also exported for using mayktso as an expres() + app = createServer(config); app.use(function(req, res, next) { res.header('X-Powered-By', mayktsoURI); @@ -1422,4 +1425,5 @@ parseLinkHeader, parseProfileLinkRelation, getBaseURL, getExternalBaseURL, +app }