Skip to content

Commit

Permalink
✨ Private RSS feed (#9088)
Browse files Browse the repository at this point in the history
refs #9001

When a blog is in private mode there is now an unguessable URL that allows access to the RSS feed for internal use, commenting systems, etc.

- add public hash for private blogging
  - auto generate on bootstrap if missing
  - global hash, we can re-use in the future
- update private blogging middleware to detect the private RSS URL and rewrite it so that the normal rss route/code is used for display
- if a normal `/rss/` route is accessed with a private session return a 404
  • Loading branch information
kirrg001 authored and kevinansfield committed Oct 5, 2017
1 parent 7be165d commit 7800ed3
Show file tree
Hide file tree
Showing 4 changed files with 136 additions and 3 deletions.
21 changes: 20 additions & 1 deletion core/server/apps/private-blogging/lib/middleware.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ var fs = require('fs'),
path = require('path'),
config = require('../../../config'),
utils = require('../../../utils'),
errors = require('../../../errors'),
i18n = require('../../../i18n'),
settingsCache = require('../../../settings/cache'),
privateRoute = '/' + config.get('routeKeywords').private + '/',
Expand Down Expand Up @@ -57,7 +58,25 @@ privateBlogging = {
});
}

privateBlogging.authenticatePrivateSession(req, res, next);
// CASE: Allow private RSS feed urls.
// Any url which contains the hash and the postfix /rss is allowed to access a private rss feed without
// a session. As soon as a path matches, we rewrite the url. Even Express uses rewriting when using `app.use()`.
if (req.url.indexOf(settingsCache.get('public_hash') + '/rss') !== -1) {
req.url = req.url.replace(settingsCache.get('public_hash') + '/', '');
return next();
}

// NOTE: Redirect to /private if the session does not exist.
privateBlogging.authenticatePrivateSession(req, res, function onSessionVerified() {
// CASE: RSS is disabled for private blogging e.g. they create overhead
if (req.path.lastIndexOf('/rss/', 0) === 0 || req.path.lastIndexOf('/rss/') === req.url.length - 5) {
return next(new errors.NotFoundError({
message: i18n.t('errors.errors.pageNotFound')
}));
}

next();
});
},

authenticatePrivateSession: function authenticatePrivateSession(req, res, next) {
Expand Down
111 changes: 110 additions & 1 deletion core/server/apps/private-blogging/tests/middleware_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@
var should = require('should'), // jshint ignore:line
sinon = require('sinon'),
crypto = require('crypto'),
settingsCache = require('../../../settings/cache'),
fs = require('fs'),
errors = require('../../../errors'),
settingsCache = require('../../../settings/cache'),
privateBlogging = require('../lib/middleware'),
sandbox = sinon.sandbox.create();

Expand Down Expand Up @@ -223,6 +224,114 @@ describe('Private Blogging', function () {
privateBlogging.authenticateProtection(req, res, next);
res.redirect.called.should.be.true();
});

it('filterPrivateRoutes should 404 for /rss/ requests', function () {
var salt = Date.now().toString();
req.url = req.path = '/rss/';

req.session = {
token: hash('rightpassword', salt),
salt: salt
};

res.isPrivateBlog = true;
res.redirect = sandbox.spy();

privateBlogging.filterPrivateRoutes(req, res, next);
next.called.should.be.true();
(next.firstCall.args[0] instanceof errors.NotFoundError).should.eql(true);
});

it('filterPrivateRoutes should 404 for /rss requests', function () {
var salt = Date.now().toString();
req.url = req.path = '/rss';

req.session = {
token: hash('rightpassword', salt),
salt: salt
};

res.isPrivateBlog = true;
res.redirect = sandbox.spy();

privateBlogging.filterPrivateRoutes(req, res, next);
next.called.should.be.true();
(next.firstCall.args[0] instanceof errors.NotFoundError).should.eql(true);
});

it('filterPrivateRoutes should 404 for tag rss requests', function () {
var salt = Date.now().toString();
req.url = req.path = '/tag/welcome/rss/';

req.session = {
token: hash('rightpassword', salt),
salt: salt
};

res.isPrivateBlog = true;
res.redirect = sandbox.spy();

privateBlogging.filterPrivateRoutes(req, res, next);
next.called.should.be.true();
(next.firstCall.args[0] instanceof errors.NotFoundError).should.eql(true);
});

it('filterPrivateRoutes: allow private /rss/ feed', function () {
settingsStub.withArgs('public_hash').returns('777aaa');

req.url = req.originalUrl = req.path = '/777aaa/rss/';
req.params = {};

res.isPrivateBlog = true;
res.locals = {};

privateBlogging.filterPrivateRoutes(req, res, next);
next.called.should.be.true();
req.url.should.eql('/rss/');
});

it('filterPrivateRoutes: allow private /rss feed', function () {
settingsStub.withArgs('public_hash').returns('777aaa');

req.url = req.originalUrl = req.path = '/777aaa/rss';
req.params = {};

res.isPrivateBlog = true;
res.locals = {};

privateBlogging.filterPrivateRoutes(req, res, next);
next.called.should.be.true();
req.url.should.eql('/rss');
});

it('filterPrivateRoutes: allow private rss feed e.g. tags', function () {
settingsStub.withArgs('public_hash').returns('777aaa');

req.url = req.originalUrl = req.path = '/tag/getting-started/777aaa/rss/';
req.params = {};

res.isPrivateBlog = true;
res.locals = {};

privateBlogging.filterPrivateRoutes(req, res, next);
next.called.should.be.true();
req.url.should.eql('/tag/getting-started/rss/');
});

it('[failure] filterPrivateRoutes: allow private rss feed e.g. tags', function () {
settingsStub.withArgs('public_hash').returns('777aaa');

req.url = req.originalUrl = req.path = '/tag/getting-started/rss/';
req.params = {};

res.isPrivateBlog = true;
res.locals = {};

res.redirect = sandbox.spy();

privateBlogging.filterPrivateRoutes(req, res, next);
res.redirect.called.should.be.true();
});
});
});
});
Expand Down
3 changes: 3 additions & 0 deletions core/server/data/schema/default-settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,9 @@
},
"password": {
"defaultValue": ""
},
"public_hash": {
"defaultValue": null
}
}
}
4 changes: 3 additions & 1 deletion core/server/models/settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ var Settings,
Promise = require('bluebird'),
_ = require('lodash'),
uuid = require('uuid'),
crypto = require('crypto'),
ghostBookshelf = require('./base'),
errors = require('../errors'),
events = require('../events'),
Expand All @@ -19,7 +20,8 @@ function parseDefaultSettings() {
var defaultSettingsInCategories = require('../data/schema/').defaultSettings,
defaultSettingsFlattened = {},
dynamicDefault = {
db_hash: uuid.v4()
db_hash: uuid.v4(),
public_hash: crypto.randomBytes(15).toString('hex')
};

_.each(defaultSettingsInCategories, function each(settings, categoryName) {
Expand Down

0 comments on commit 7800ed3

Please sign in to comment.