diff --git a/core/server/errors/index.js b/core/server/errors/index.js index 288c0e4f6d..9cf719dd61 100644 --- a/core/server/errors/index.js +++ b/core/server/errors/index.js @@ -9,6 +9,7 @@ var _ = require('lodash'), BadRequestError = require('./bad-request-error'), InternalServerError = require('./internal-server-error'), NoPermissionError = require('./no-permission-error'), + MethodNotAllowedError = require('./method-not-allowed-error'), RequestEntityTooLargeError = require('./request-too-large-error'), UnauthorizedError = require('./unauthorized-error'), ValidationError = require('./validation-error'), @@ -359,3 +360,4 @@ module.exports.RequestEntityTooLargeError = RequestEntityTooLargeError; module.exports.UnsupportedMediaTypeError = UnsupportedMediaTypeError; module.exports.EmailError = EmailError; module.exports.DataImportError = DataImportError; +module.exports.MethodNotAllowedError = MethodNotAllowedError; diff --git a/core/server/errors/method-not-allowed-error.js b/core/server/errors/method-not-allowed-error.js new file mode 100644 index 0000000000..dcb28c129f --- /dev/null +++ b/core/server/errors/method-not-allowed-error.js @@ -0,0 +1,14 @@ +// # Not found error +// Custom error class with status code and type prefilled. + +function MethodNotAllowedError(message) { + this.message = message; + this.stack = new Error().stack; + this.code = 405; + this.type = this.name; +} + +MethodNotAllowedError.prototype = Object.create(Error.prototype); +MethodNotAllowedError.prototype.name = 'MethodNotAllowedError'; + +module.exports = MethodNotAllowedError; diff --git a/core/server/middleware/index.js b/core/server/middleware/index.js index 9b9aa5f587..908bdd7a6b 100644 --- a/core/server/middleware/index.js +++ b/core/server/middleware/index.js @@ -217,7 +217,8 @@ function serveSharedFile(file, type, maxAge) { setupMiddleware = function setupMiddleware(blogAppInstance, adminApp) { var logging = config.logging, corePath = config.paths.corePath, - oauthServer = oauth2orize.createServer(); + oauthServer = oauth2orize.createServer(), + apiRouter; // silence JSHint without disabling unused check for the whole file authStrategies = authStrategies; @@ -310,7 +311,19 @@ setupMiddleware = function setupMiddleware(blogAppInstance, adminApp) { // ### Routing // Set up API routes - blogApp.use(routes.apiBaseUri, routes.api(middleware)); + apiRouter = routes.api(middleware); + blogApp.use(routes.apiBaseUri, apiRouter); + // ### Invalid method call on valid route + apiRouter.use(function (req, res, next) { + apiRouter.stack.forEach(function (item) { + if (item.regexp.test(req.path) && item.route !== undefined) { + return next(new errors.MethodNotAllowedError('Method not allowed')); + } + }); + + // Didn't match any path -> 404 + res.status(404).json({errors: {type: 'NotFoundError', message: 'Unknown API endpoint.'}}); + }); // Mount admin express app to /ghost and set up routes adminApp.use(middleware.redirectToSetup); diff --git a/core/test/functional/routes/api/base_test.js b/core/test/functional/routes/api/base_test.js new file mode 100644 index 0000000000..fef58173d9 --- /dev/null +++ b/core/test/functional/routes/api/base_test.js @@ -0,0 +1,60 @@ +/*global describe, it, before, after */ +/*jshint expr:true*/ +var supertest = require('supertest'), + testUtils = require('../../../utils'), + ghost = require('../../../../../core'), + request; + +describe('API', function () { + var accesstoken = ''; + + before(function (done) { + // starting ghost automatically populates the db + // TODO: prevent db init, and manage bringing up the DB with fixtures ourselves + ghost().then(function (ghostServer) { + request = supertest.agent(ghostServer.rootApp); + }).then(function () { + return testUtils.doAuth(request); + }).then(function (token) { + accesstoken = token; + done(); + }).catch(function (e) { + console.log('Ghost Error: ', e); + console.log(e.stack); + }); + }); + + after(function (done) { + testUtils.clearData().then(function () { + done(); + }).catch(done); + }); + + it('should return 404 when using invalid path', function (done) { + request.get(testUtils.API.getApiQuery('invalidpath/')) + .set('Authorization', 'Bearer ' + accesstoken) + .expect(404) + .end(function (err, res) { + /*jshint unused:false*/ + if (err) { + return done(err); + } + + done(); + }); + }); + + it('should return 405 Method not allowed when invalid method is used', function (done) { + request.put(testUtils.API.getApiQuery('db/')) + .set('Authorization', 'Bearer ' + accesstoken) + .expect(405) + .end(function (err, res) { + /*jshint unused:false*/ + if (err) { + return done(err); + } + + done(); + }); + }); +});