Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to implement a bespoke recaptcha in rails?

Though using existing gems to add a recaptcha to every single form is a pretty simple affair, I can't work out how to create recaptcha as described by the diagram below.

      |
 user submits question form  <-------
      |                             | 
      V                             |
=============================       |
| Are the attributes valid? | ----> NO 
=============================
      |
     YES  
      |
      V
===============================================================
| Has user submitted more than 4 forms in the last 5 minutes? | --NO---| 
===============================================================        |
     |                                                                 |
    YES                                                                |
     |                                                                 |
  redirect them to recaptcha form                                      |
     |                                                                 |
     V                                                                 V
============================                    ************************************
|  Captcha form incorrect? | -- NO ---------->  *   save the form as a record      *
============================                    *     and redirect user to see it  *
     |              ^                           ************************************
    YES             |
     |              NO
     V              |
==================================================
|  Have they been redirected to the captcha form | 
|    more than 5 times in the last minute?       |
==================================================
     |
    YES
     |
     V
*******************
* TEMPORARY BLOCK *
*******************          

I just can't do figure out how to do it. Where is the submitted data saved, for example, while the user fills out the captcha form? In the session? In the cache? In a cookie? Any pointers on how this could be achieved?

I am thinking some sort of function in the application controller, triggered by a before_action all hook. Every time this action is called (i.e whenever a user requests a link of my app) the action is time-stamped in an array of time stamps. When this array is filled with say 10 time stamps, we work out the average, and it's under x amount we block them. This would stop crawlers as well as spammers.

However, I still want to let google, yahoo! and bing in! And why not the internet way back machine? I don't want to compromise my app. There's lots of little nuances I need to know before I jump into this.

Here's the behavior I want to implement in a nutshell:

*If the user is sending x number of requests in y number of seconds, redirect them to a recaptcha page. If they pass the recaptcha, let them continue as normal. *

I'm open to both visual and logic (question based) recaptchas, just want a solution other than adding a recaptcha to every single form...

Also, I don't like using Gems as they feel like blackboxes to me. Much prefer doing it myself. However, I do like versitile gems like state machine and carrierwave, so if there's an open ended-recaptcha gem I'm open to that.

like image 937
Starkers Avatar asked Oct 02 '22 18:10

Starkers


1 Answers

I can't work out how to create recaptcha as described by the diagram below.

This is bit difficult question. I will try to make it easier.

Before you read further, may be you should check this gem https://github.com/kickstarter/rack-attack , with this gem you can Whitelists , Blacklists , Throttles the requests. But it is middleware so you can't use it directly since your condition includes validating of data .

To tackle your condition, you have to write your own library which will do tracking and block the request.

I just can't do figure out how to do it. Where is the submitted data saved, for example, while the user fills out the captcha form? In the session? In the cache? In a cookie? Any pointers on how this could be achieved?

You should save the data in session or you can also use cookies. I recommend you to use session.

I am thinking some sort of function in the application controller, triggered by a before_action all hook. Every time this action is called (i.e whenever a user requests a link of my app) the action is time-stamped in an array of time stamps. When this array is filled with say 10 time stamps, we work out the average, and it's under x amount we block them. This would stop crawlers as well as spammers.

Here it comes a tricky part. The idea is simple. When request comes in, we will track the request and as soon as it hits the limit in a given period , we will block it. To explain the value of both of this parameter consider this situation when you want to limit 50 requests in every 10.minutes. so

 period = 50
 limit = 10.minutes
  1. Yes, you need some sort of before_action callback which will track all methods.
  2. To save the requests we will use cache. Since Rails.cache is fast and we can easily expire (or remove) the value after certain period with expires_in option provided in Rails.cache

You have to do something like this in your application_controller

before_action :track_requests

 private
  def allowed?
    @tracker.allowed?
   end

  def track_requests
    @tracker = ::TrackRequests.new('Track Based On Ip',request.ip ,:limit => 1, :period => 10.minutes)
  end

In TrackRequests you have to pass some essential parameters its name , identity limit , period option.

  1. name: it is just a namespace. It can be anything any name which could make sense.

  2. match: It can be anything will provide user identity. it can be email or ip.

  3. options: Next thing you have limit and period option. it is just an expanded way of saying you want x requests in y minutes.

The TrackRequests call is responsible for saving the requests in cache, getting the request from it, incrementing the request by 1 when same request appears again in a given period and checking the validity of request.

class TrackRequests

  def initialize(name,match, options)
   @name = name
   @match = match
   @limit = options[:limit]
   @period = options[:period]
   @prefix = 'TrackRequests'
   save_the_request_in_cache
   cache_read
 end

 def allowed? //for checking validity 
   cache_read < @limit
 end

  def cache_read // for reading the cache 
    Rails.cache.read(unique_key)
  end

   def save_the_request_in_cache // saving the data into cache 
    epoch_time = Time.now.to_i
    expires_in = @period - (epoch_time % @period) 
   // this will be the span of time between expire time and current_time. this value will keep decrementing whenever new requests came into it
  if  cache_read 
     // if it 2nd or 3rd request , 
     // its value will be automatically incremented by one. 
     // The incrementing values are the number of request received in a give time period.
     // if this will goes above limit, it will block  further requests.
      Rails.cache.increment(unique_key, 1, :expires_in => expires_in) 
   else
      // if the it very first request, then you have to create the cahce 
     Rails.cache.write(unique_key, 1, :expires_in => expires_in) 
   end
  end
  // to get the unique key to cache. Remember that, this key supposed to be something which should be unique and can be recognisable between same requests. For xyz IP, it will be foobar and always will be.  
  def unique_key
   @key ||= "#{@prefix}:#{@match}#{@name}"
  end

end

And, finally you have to do something like this in your controller

def create @post = Post.new(post_params)

if @post.valid?
  if allowed? && @post.save
    redirect_to @post, notice: 'Post was successfully created.'
  else
     cookies[:url] = request.env["HTTP_REFERER"]
    redirect_to :captcha , notice: 'Please type captcha' 
  end
else
  render action: 'new'
end

However, I still want to let google, yahoo! and bing in! And why not the internet way back machine? I don't want to compromise my app. There's lots of little nuances I need to know before I jump into this.

Okay. No problem. It wont block this search engines , but still i recommend you to all those those search engine user-agent in whiltelist (may be my above gem)

I'm open to both visual and logic (question based) recaptchas, just want a solution other than adding a recaptcha to every single form...

No problem, you can add attach allowed? with valid? or save method. Or you can use before_save method .

Also, I don't like using Gems as they feel like blackboxes to me. Much prefer doing it myself. However, I do like versitile gems like state machine and carrierwave, so if there's an open ended-recaptcha gem I'm open to that

I appreciate it. state_machine is good option.

Lastly, consider above code as algorithms rather then actual implemented code. This code might have some silly bug but if you understand the idea you can easily roll your own library.

That's all.

like image 149
Paritosh Piplewar Avatar answered Oct 13 '22 10:10

Paritosh Piplewar