hapijs / crumb

CSRF crumb generation and validation for hapi
Other
171 stars 50 forks source link

Adding crumb breaks onPreResponse Header() #95

Closed ghost closed 6 years ago

ghost commented 7 years ago

As soon as I register the crumb plugin, an error TypeError: Uncaught Error: request.response.header() is not a function.

server.js

server.register([
    //UTILS
    require('./Config/Auth/jwt'),
    require('./Config/RateLimiter/RateLimiter'),
    {
        register: require('crumb'),
        options: {
            key: 'csrf',
            restful: true
        }
    },

    //ROUTES
    require('./Controllers/AuthController'),
    require('./Controllers/BookController'),
    require('./Controllers/UserController')
],
(err) => {
    if (err) {
        console.error('Failed to load a plugin:', err);
    }
});

RateLimiter.js

'use strict'

const throttle = require('./ThrottleController')

const register = function register (server, options, next) {
  server.ext({
    type: 'onPreResponse',
    //type: 'onRequest',
    method: throttle.limit
});
  //next()
}

register.attributes = {
  name: 'rate-limiter',
  version: '1.0'
}

module.exports = register

Throttler.js

const Boom = require('boom');

// Module dependencies
const config = require('./RateConfig'),  
      mongoose = require('mongoose');

// Load model
const RateBuckets = require('./RateBucket');

exports.limit = function(request, response, next) {  
    'use strict';
    var ip = request.info.remoteAddress;

    RateBuckets
        .findOneAndUpdate({ip: ip}, { $inc: { hits: 1 } }, { upsert: false })
        .exec(function(error, rateBucket) {
            if (error) {
                return response(Boom.badRequest(error));
            }
            if(!rateBucket) {
                rateBucket = new RateBuckets({
                    createdAt: new Date(),
                    ip: ip
                });
                rateBucket.save(function(error, rateBucket) {
                    if (error) {
                        return response(Boom.badRequest(error));
                    }
                    if(!rateBucket) {
                        return response(Boom.badImplementation('Cant\' create rate limit bucket'));
                    }
                    var timeUntilReset = config.rateLimits.ttl - (new Date().getTime() - rateBucket.createdAt.getTime());
                    console.log(JSON.stringify(rateBucket, null, 4));
                    // the rate limit ceiling for that given request
                    request.response.header('X-Rate-Limit-Limit', config.rateLimits.maxHits);
                    // the number of requests left for the time window
                    request.response.header('X-Rate-Limit-Remaining', config.rateLimits.maxHits - 1);
                    // the remaining window before the rate limit resets in miliseconds
                    request.response.header('X-Rate-Limit-Reset', timeUntilReset);
                    // Return bucket so other routes can use it
                    request.rateBucket = rateBucket;
                    //return next();
                    return response.continue();
                });
            } else {
                var timeUntilReset = config.rateLimits.ttl - (new Date().getTime() - rateBucket.createdAt.getTime());
                var remaining =  Math.max(0, (config.rateLimits.maxHits - rateBucket.hits));
                console.log(JSON.stringify(rateBucket, null, 4));
                // the rate limit ceiling for that given request
                request.response.header('X-Rate-Limit-Limit', config.rateLimits.maxHits);
                // the number of requests left for the time window
                request.response.header('X-Rate-Limit-Remaining', remaining);
                // the remaining window before the rate limit resets in miliseconds
                request.response.header('X-Rate-Limit-Reset', timeUntilReset);
                // Return bucket so other routes can use it
                request.rateBucket = rateBucket;
                // Reject or allow
                if(rateBucket.hits < config.rateLimits.maxHits) {
                    //return next();
                    return response.continue();
                } else {
                    return response(Boom.tooManyRequests('You have exceeded your request limit.'));
                }
            }
        });

};
jonathansamines commented 7 years ago

I dont think this is an issue with this plugin. From the hapi documentation, seems like at the "onPreResponse" extension point, the request.response object can either be a "boom error" or an actual response object, in case it is a "boom error" then the object does not have the "header" method because it is not a response object, but a boom error. I think you should check for the isBoom property, and use reply.continue() in such cases to avoid the error.

See:

jonathansamines commented 7 years ago

The reason you are getting the error, is likely because crumb sometimes responds with "boom errors".

jonathansamines commented 6 years ago

Closing due inactivity. Please feel free to open this again if none of my suggestions worked and the issue persists.

Also note that, the reason you are getting boom errors when registering this plugin, is likely because your api request are not sending the proper csrf token. Anyway, as I mentioned before the actual issue is caused because errors are not being checked at the extension point.