Simple User Registration Spam Prevention in Rails 3
October 5th, 2011 // 10:42 pm @ matt
Tonight, in a little under an hour, I was able to implement spam prevention for my user registration flow thanks to the simple API provided by Stop-Registration-Spam.org.
In Rails 3, creating new validators is relatively painless. Create a new validator class in a folder defined by you. For my project, I decided to place all validators within app/validators.
The class should extend the ActiveModel::EachValidator class found within Rails 3 core, in which you’ll generally override two methods: check_validity! and validate_each.
check_validity!is called by the class’ initializer to verify that the validator’s arguments supplied are valid. If they are not, throw an exception that will be seen by the developer using your validator.validate_eachis where the actual validation occurs on the values set for each attribute implementing this validator at runtime.
The code for the validator is included in the code snippet below.
class SpamValidator < ActiveModel::EachValidator DEFAULTS = {:level => 1, :timeout => 5}.freeze MESSAGES = {:block => "is blacklisted", :no_domain => "includes invalid domain", :ip_not_recognized => nil, :over_limit => nil}.freeze RESERVED_OPTIONS = [:level, :timeout, :block, :no_domain, :ip_not_recognized, :over_limit].freeze def initialize(options) DEFAULTS.each do |key, value| options[key] ||= value end super(options) end def check_validity! unless options[:level].is_a?(Integer) && (1..5).include?(options[:level]) raise ArgumentError, ":level must be an Integer between 1-5" end unless options[:timeout].is_a?(Integer) && options[:timeout] > 0 raise ArgumentError, ":timeout must be an Integer greater than 0" end end def validate_each(record, attribute, value) return if value.nil? raise ArgumentError, "#{attribute} must be a String." unless value.is_a?(String) begin # API REF: http://www.stop-registration-spam.org/api url = "http://www.stop-registration-spam.org/api/level#{options[:level]}/json?email=#{CGI.escape(value)}" result = JSON.parse(open(url, :read_timeout => options[:timeout]).read) rescue # TODO log error end if result && result['request_status'] error_key = result['request_status'].to_s.downcase.to_sym error_message = MESSAGES[error_key] if error_message errors_options = options.except(*RESERVED_OPTIONS) errors_options[:message] ||= options[error_key] if options[error_key] record.errors.add(attribute, error_message, errors_options) end end end end
Using this validator is incredibly easy. In your model, just add a :spam key with whatever values you require passed as a hash within the validates call for your email-formatted attributes.
class User < ActiveRecord::Base validates :email, :spam => { :level => 5 } end
The hash accepts the following parameters:
:level– integer from 1-5 (default 1) – The level of spam prevention to utilize as defined by the Stop-Registration-Spam.org API.:timeout– integer greater than 0 (default 5) – The number of seconds to timeout when unable to connect with, or receive a response from, the API.:block– string (default “is blacklisted”) – The message to use for response code BLOCK, meaning this email address did not meet the checks at the level requested.:no_domain– string (default “includes invalid domain”) – The message to use for response code NO_DOMAIN, meaning the domain provided is not valid.:ip_not_recognized– string (defaultnil) – The message to use for response code IP_NOT_RECOGNISED, meaning the requesting IP is not valid for high volume commercial use.:over_limit– string (defaultnil) – The message to use for response code OVER_LIMIT, meaning you have reached your daily request limit.
My decision to leave :ip_not_recognized and :over_limit as nil values with no error messages is due to my not wanting to prevent users from registering when the service is not responding as expected. Similarly, the validator passes when a timeout occurs. This could easily be modified to allow for further customizations through additional hash values, but this is suitable for my needs, and it demonstrates the simplicity behind implementing a custom Rails 3 validator.