module ActiveHashcash

ActiveHashcash protects Rails applications against bots and brute force attacks without annoying humans. See the README.md for more explanations about Hashcash.

Include this module into your Rails controller.

class SessionController < ApplicationController
  include ActiveHashcash

  before_action :check_hashcash, only: :create
end

You are welcome to override most of the methods to customize to your needs. For example, if your app runs behind a load balancer you should probably override hashcash_ip_address.

Constants

VERSION

Public Class Methods

# File lib/active_hashcash.rb, line 50
def self.throttle_rules=(rules)
  rules&.each do |rule|
    raise ArgumentError, "ActiveHashcash.throttle_rules period must be present" if rule[:period].nil?
    raise ArgumentError, "ActiveHashcash.throttle_rules rate must be >= 0" if rule[:rate].to_f.negative?
  end
  @@throttle_rules = rules && rules.sort_by { |rule| rule[:period] }
end

Public Instance Methods

Call that method via a before_action when the form is submitted:

before_action :check_hashcash, only: :create

In case of invalid hashcash it calls hashcash_after_failure that you can override. Otherwise, hashcash stamp is stored in database to prevent from double spending.

# File lib/active_hashcash.rb, line 74
def check_hashcash
  attrs = {
    ip_address: hashcash_ip_address,
    request_path: hashcash_request_path,
    context: hashcash_stamp_context
  }
  if hashcash_param && Stamp.spend(hashcash_param, hashcash_resource, hashcash_bits, Date.yesterday, attrs)
    hashcash_after_success
  else
    hashcash_after_failure
  end
end

That method is called when check_hashcash fails. It raises ActionController::InvalidAuthenticityToken so HTTP response will be 422 by default. Override this method to provide a different behaviour.

# File lib/active_hashcash.rb, line 145
def hashcash_after_failure
  raise ActionController::InvalidAuthenticityToken.new("Invalid hashcash #{hashcash_param}")
end

Maybe you want something special when the hashcash is valid. By default nothing happens.

# File lib/active_hashcash.rb, line 150
def hashcash_after_success
  # Override me for your own needs.
end

Returns the complexity, the higher the slower it is. Eventually adds penalties for bad and pushy IPs.

# File lib/active_hashcash.rb, line 114
def hashcash_bits
  (ActiveHashcash.bits + hashcash_throttle_penalty + hashcash_reputation_penalty).floor
end

Call it inside the form that have to be protected and don’t forget to initialize the JavaScript Hashcash.setup(). Unless you need something really special, you should not need to override this method.

<% form_for model do |form| %>
  <%= hashcash_hidden_field_tag %>

<% end %>
# File lib/active_hashcash.rb, line 162
def hashcash_hidden_field_tag(name = :hashcash)
  options = {
    resource: hashcash_resource,
    bits: hashcash_bits,
    waiting_message: hashcash_waiting_message,
    date: Date.today.strftime("%y%m%d")
  }
  view_context.hidden_field_tag(name, "", "data-hashcash" => options.to_json)
end

Returns remote IP address. They are used to automatically increase complexity when the same IP sends many valid hashcash. If you’re app is behind a load balancer, you should probably override it to read the right HTTP header.

# File lib/active_hashcash.rb, line 90
def hashcash_ip_address
  request.remote_ip
end

Override if you want to rename the hashcash param.

# File lib/active_hashcash.rb, line 133
def hashcash_param
  params[:hashcash]
end

Compute a reputation penalty for the current IP.

# File lib/active_hashcash.rb, line 128
def hashcash_reputation_penalty
  Reputation.scores(hashcash_ip_address).values.sum * 4
end

Return current request path to be saved to the successful ActiveHashcash::Stamp. If multiple forms are protected via hashcash this is an interesting info.

# File lib/active_hashcash.rb, line 96
def hashcash_request_path
  request.path
end

This is the resource used to build the hashcash stamp. By default the host name is returned. It should be good for most cases and prevent from reusing the same stamp between sites.

# File lib/active_hashcash.rb, line 108
def hashcash_resource
  ActiveHashcash.resource || request.host
end

Override this method to store custom data for each stamp. It must returns a hash or nil.

# File lib/active_hashcash.rb, line 102
def hashcash_stamp_context
end

Compute a penalty for pushy IPs. The penalty rules can be defined with ‘ActiveHashcash.throttle_rules`.

# File lib/active_hashcash.rb, line 120
def hashcash_throttle_penalty
  rules = ActiveHashcash.throttle_rules || []
  periods = rules.map { |rule| rule[:period] }
  counts = ActiveHashcash::Stamp.where(ip_address: hashcash_ip_address).sum_by_periods(periods)
  rules.each_with_index.sum { |rule, index| counts[index].to_i * rule[:rate].to_f }
end

Override to customize message displayed on submit button while computing hashcash.

# File lib/active_hashcash.rb, line 138
def hashcash_waiting_message
  t("active_hashcash.waiting_label")
end