Skip to content
This repository has been archived by the owner on Oct 5, 2020. It is now read-only.

Commit

Permalink
Merge pull request #449 from joemfb/rest-proxy
Browse files Browse the repository at this point in the history
adds a new, explicitly white-listed REST API proxy
  • Loading branch information
grtjn authored Feb 3, 2017
2 parents 29450ae + f1e7895 commit d19e35e
Show file tree
Hide file tree
Showing 2 changed files with 115 additions and 131 deletions.
245 changes: 114 additions & 131 deletions app/templates/node-server/proxy.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,152 +2,135 @@

'use strict';

var httpProxy = require('http-proxy');
var router = require('express').Router();
var url = require('url');

var authHelper = require('./utils/auth-helper');
var http = require('http');
var options = require('./utils/options')();

// ==================================
// MarkLogic REST API endpoints
// ==================================

//
// To not require authentication for a specific route, simply use the route below.
// Copy and change according to your needs.
//
//router.get('/my/route', function(req, res) {
// noCache(res);
// proxy(req, res);
//});

// For any other GET request, proxy it on to MarkLogic.
router.get('*', function(req, res) {
noCache(res);
if (!(options.guestAccess || req.isAuthenticated())) {
res.status(401).send('Unauthorized');
} else {
proxy(req, res);
}
});
/************************************************/
/************* setup proxy server *************/
/************************************************/

// PUT requires special treatment, as a user could be trying to PUT a profile update..
router.put('*', function(req, res) {
noCache(res);
// For PUT requests, require authentication
if (!req.isAuthenticated()) {
res.status(401).send('Unauthorized');
} else if (options.disallowUpdates || ((req.path === '/documents') &&
req.query.uri &&
req.query.uri.match('/api/users/') &&
!req.query.uri.match('/api/users/' + req.session.passport.user.username + '.json'))) {
// The user is trying to PUT to a profile document other than his/her own. Not allowed.
res.status(403).send('Forbidden');
} else {
// proxy original request
proxy(req, res);
}
// TODO: configurable path?
var target = url.format({
protocol: 'http',
hostname: options.mlHost,
port: options.mlHttpPort,
pathname: '/v1'
});

// Require authentication for POST requests
router.post(/^\/(alert\/match|search|suggest|values\/.*)$/, function(req, res) {
noCache(res);
var proxyServer = httpProxy.createProxyServer({ target: target });

function getAuth(req) {
var user = req.session.passport && req.session.passport.user &&
req.session.passport.user.username;

return authHelper.getAuthorization(req.session, req.method, req.path, {
authUser: user
})
}

function proxy (req, res) {
getAuth(req).then(function (auth) {
// TODO: if no auth?
var headers = { headers: { authorization: auth } };

// TODO: filter www-header in response?
// (currently prompts without authed middleware)

proxyServer.web(req, res, headers, function (e) {
console.log(e);
res.status(500).send('Error');
});
}, function (e) {
console.log('auth error:');
console.log(e);
return res.status(401).send('Unauthorized');
});
}

/************************************************/
/********** create custom middleware **********/
/************************************************/

function noCache (req, res, next) {
res.append('Cache-Control', 'no-cache, must-revalidate'); // HTTP 1.1 - must-revalidate
res.append('Pragma', 'no-cache'); // HTTP 1.0
res.append('Expires', 'Sat, 26 Jul 1997 05:00:00 GMT'); // Date in the past

next();
}

function authed (req, res, next) {
if (!(options.guestAccess || req.isAuthenticated())) {
res.status(401).send('Unauthorized');
} else {
proxy(req, res);
return res.status(401).send('Unauthorized');
}
});

router.post('*', function(req, res) {
noCache(res);
if (!req.isAuthenticated()) {
res.status(401).send('Unauthorized');
} else if (options.disallowUpdates) {
res.status(403).send('Forbidden');
} else {
proxy(req, res);
}
});
next();
}

// (#176) Require authentication for DELETE requests
router.delete('*', function(req, res) {
noCache(res);
if (!req.isAuthenticated()) {
res.status(401).send('Unauthorized');
} else if (options.disallowUpdates) {
res.status(403).send('Forbidden');
} else {
proxy(req, res);
function update (req, res, next) {
if (options.disallowUpdates) {
return res.status(403).send('Forbidden');
}
});

// Generic proxy function used by multiple HTTP verbs
function proxy(req, res) {
var queryString = req.originalUrl.split('?')[1];
var path = req.baseUrl + req.path + (queryString ? '?' + queryString : '');
console.log(
req.method + ' ' + req.path + ' proxied to ' +
options.mlHost + ':' + options.mlHttpPort + path);
var reqOptions = {
hostname: options.mlHost,
port: options.mlHttpPort,
method: req.method,
path: path,
headers: req.headers
};

var passportUser = req.session.passport && req.session.passport.user;
authHelper.getAuthorization(req.session, reqOptions.method, reqOptions.path,
{
authUser: passportUser && passportUser.username
}
).then(
function(authorization) {
if (authorization) {
reqOptions.headers.Authorization = authorization;
}
var mlReq = http.request(reqOptions, function(response) {

res.statusCode = response.statusCode;

// [GJo] (#67) forward all headers from MarkLogic
for (var header in response.headers) {
if (!/^WWW\-Authenticate$/i.test(header)) {
res.header(header, response.headers[header]);
}
}

response.on('data', function(chunk) {
res.write(chunk);
});
response.on('end', function() {
res.end();
});
});

req.pipe(mlReq);
req.on('end', function() {
mlReq.end();
});

mlReq.on('socket', function (socket) {
socket.on('timeout', function() {
console.log('Timeout reached, aborting call to ML..');
mlReq.abort();
});
});

mlReq.on('error', function(e) {
console.log('Proxying failed: ' + e.message);
res.status(500).end();
});
});
next();
}

function noCache(response) {
response.append('Cache-Control', 'no-cache, must-revalidate'); // HTTP 1.1 - must-revalidate
response.append('Pragma', 'no-cache'); // HTTP 1.0
response.append('Expires', 'Sat, 26 Jul 1997 05:00:00 GMT'); // Date in the past
function profile (req, res, next) {
if ((req.path === '/documents') &&
req.query.uri &&
req.query.uri.match('/api/users/') &&
!req.query.uri.match('/api/users/' + req.session.passport.user.username + '.json')) {
return res.status(403).send('Forbidden');
}

next();
}

/************************************************/
/************ configure middleware ************/
/************************************************/

// allow any, unauthed:
// router.use(proxy);

router.use(noCache);
router.use(authed);

/************************************************/
/************** configure routes **************/
/************************************************/

router.get('/config/query/*', proxy);

router.get('/graphs/sparql', proxy);

var search = router.route('/search');
search.get(proxy);
search.post(proxy);

var suggest = router.route('/suggest');
suggest.get(proxy);
suggest.post(proxy);

var values = router.route('/values');
values.get(proxy);
values.post(proxy);

var docs = router.route('/documents');
docs.get(proxy);
docs.all(update, profile, proxy);

var ext = router.route('/resources/*');
ext.get(proxy);
ext.all(update, proxy);

// Explicitly reject all other routes
router.all('*', function (req, res) {
res.status(401).send('Not proxied');
});

module.exports = router;
1 change: 1 addition & 0 deletions app/templates/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"express": "^4.4.1",
"express-session": "^1.5.0",
"helmet": "^2.0.0",
"http-proxy": "^1.16.2",
"morgan": "^1.6.0",
"passport": "^0.3.2",
"passport-local": "^1.0.0",
Expand Down

0 comments on commit d19e35e

Please sign in to comment.