Skip to content

Rack middleware - Automatically block requests from suspicious IPs #3

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,5 @@ source "http://rubygems.org"
gem "rspec"
gem "flexmock"
gem "net-dns2"
gem "test-unit"
gem "rack-test"
46 changes: 33 additions & 13 deletions Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,24 +1,44 @@
GEM
remote: http://rubygems.org/
specs:
diff-lcs (1.1.2)
diff-lcs (1.2.5)
flexmock (0.8.11)
net-dns (0.6.1)
rspec (2.0.1)
rspec-core (~> 2.0.1)
rspec-expectations (~> 2.0.1)
rspec-mocks (~> 2.0.1)
rspec-core (2.0.1)
rspec-expectations (2.0.1)
diff-lcs (>= 1.1.2)
rspec-mocks (2.0.1)
rspec-core (~> 2.0.1)
rspec-expectations (~> 2.0.1)
net-dns2 (0.8.7)
packetfu
network_interface (0.0.1)
packetfu (1.1.11)
network_interface (~> 0.0)
pcaprub (~> 0.12)
pcaprub (0.12.0)
power_assert (0.2.6)
rack (1.6.4)
rack-test (0.6.3)
rack (>= 1.0)
rspec (3.4.0)
rspec-core (~> 3.4.0)
rspec-expectations (~> 3.4.0)
rspec-mocks (~> 3.4.0)
rspec-core (3.4.1)
rspec-support (~> 3.4.0)
rspec-expectations (3.4.0)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.4.0)
rspec-mocks (3.4.0)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.4.0)
rspec-support (3.4.1)
test-unit (3.1.5)
power_assert

PLATFORMS
ruby

DEPENDENCIES
flexmock
net-dns
net-dns2
rack-test
rspec
test-unit

BUNDLED WITH
1.10.6
29 changes: 28 additions & 1 deletion README.rdoc
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,34 @@ It is a handy thing to be able to identify spammers, harvesters, and other suspi

This Gem requires that you have an Http:BL API key from Project Honeypot. You can get one at http://www.projecthoneypot.org/

= Usage
= Rails Support

Automatically block requests by environment or method (for example, **POST**, **PUT**, **PATCH*, **DELETE*) to prevent malicious behavior from known spammers and the spambots. Choose a threat tolerance, for example no_tolerance, or a customized level of tolerance based on the threat score (0-255, 255 being the most threatening) or the days since last activity. You can also specify the error status code, headers and message.

== Setup

1) Add `'project-honeypot', '`~> 0.1.4'` to your Gemfile and bundle install
2) Run `rails generate project_honeypot:install` to add an initializer to your application

== Initializer Confirguration

ProjectHoneypot.configure do |config|
config.api_key = ENV['HONEYPOT_API_KEY'] # required
config.methods = ['POST', 'PUT', 'PATCH', 'DELETE'] # default
config.environments = ['production'] # default
config.no_tolerance = true # default

# - or -
# config.no_tolerance = false
# config.score_tolerance = 32 # greater than 32 threat score
# config.last_activity_tolerance = 1000 # less than a 1000 days

# config.error_status_code = 422
# config.error_headers = {'Cache-Control' => 'no-cache'}
# config.error_message = ["WARNING: This IP Address has been flagged for suspicious behavior. Be advised."]
end

= Methods

HTTP:BL lookups through Project Honeypot result in a Url object that gives you the risk score, last activity, and types of offenses the ip address is listed for.

Expand Down
8 changes: 8 additions & 0 deletions Rakefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
require 'rake'
require 'rspec/core/rake_task'

RSpec::Core::RakeTask.new(:spec) do |t|
t.pattern = Dir.glob('spec/*_spec.rb')
end
task :default => :spec
task :test => :spec
8 changes: 8 additions & 0 deletions lib/generators/install_generator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
module ProjectHoneypot
class InstallGenerator < Rails::Generators::Base
source_root(File.expand_path(File.dirname(__FILE__)))
def copy_initializer
copy_file 'project_honeypot.rb', 'config/initializers/project_honeypot.rb'
end
end
end
16 changes: 16 additions & 0 deletions lib/generators/project_honeypot.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
ProjectHoneypot.configure do |config|
config.api_key = ENV['HONEYPOT_API_KEY']
config.methods = ['POST', 'PUT', 'PATCH', 'DELETE'] # default
config.environments = ['production'] # default
config.no_tolerance = true # default

## - or -
# config.no_tolerance = false
# config.score_tolerance = 32 # greater than 32 threat score
# config.last_activity_tolerance = 1000 # less than a 1000 days

## - additional configuration options
# config.error_status_code = 422
# config.error_headers = {'Cache-Control' => 'no-cache'}
# config.error_message = ["WARNING: This IP Address has been flagged for suspicious behavior. Be advised."]
end
9 changes: 9 additions & 0 deletions lib/project-honeypot.rb
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
require 'net/dns'
require File.dirname(__FILE__) + "/project_honeypot/url.rb"
require File.dirname(__FILE__) + "/project_honeypot/base.rb"
require File.dirname(__FILE__) + "/project_honeypot/configuration.rb"
require File.dirname(__FILE__) + "/project_honeypot/railtie.rb"
require File.dirname(__FILE__) + "/project_honeypot/middleware.rb"

module ProjectHoneypot

def self.lookup(api_key, url)
searcher = Base.new(api_key)
searcher.lookup(url)
end

def self.query(opts={})
fail ArgumentError, 'You must specify an IP Address.' if opts[:ip].nil?
Base.new(opts[:api_key] || nil).lookup(opts[:ip])
end
end
5 changes: 3 additions & 2 deletions lib/project_honeypot/base.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
module ProjectHoneypot
class Base
def initialize(api_key)
@api_key = api_key
def initialize(api_key = nil)
@api_key = api_key || ProjectHoneypot.configuration.api_key
fail ArgumentError, 'You must specify an api_key.' if @api_key.nil?
end

def lookup(ip_address)
Expand Down
30 changes: 30 additions & 0 deletions lib/project_honeypot/configuration.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
module ProjectHoneypot

class << self
attr_accessor :configuration
end

def self.configure
self.configuration ||= Configuration.new
yield(configuration)
end

class Configuration
attr_accessor :api_key
attr_accessor :methods, :environments, :offenses, :no_tolerance, :score_tolerance, :last_activity_tolerance
attr_accessor :error_status_code, :error_headers, :error_message

def initialize
@api_key = nil
@methods = ['POST', 'PUT', 'PATCH', 'DELETE']
@environments = ['production']
@offenses = ['comment_spammer', 'harvester', 'suspicious']
@no_tolerance = true
@score_tolerance = 1
@last_activity_tolerance = 1
@error_status_code = 422
@error_headers = {'Cache-Control' => 'no-cache'}
@error_message = ["WARNING: This IP Address has been flagged for suspicious behavior. Be advised."]
end
end
end
46 changes: 46 additions & 0 deletions lib/project_honeypot/middleware.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
module ProjectHoneypot
class Middleware

def initialize(app)
@app = app
end

def call(env)
begin

# Validate Request :: API_KEY present, right environment, right method?
if !(ProjectHoneypot.configuration.api_key.nil?) && ProjectHoneypot.configuration.environments.include?(ENV['RACK_ENV']) && ProjectHoneypot.configuration.methods.include?(env['REQUEST_METHOD'])
request = ProjectHoneypot.query({ip: (env['HTTP_FASTLY_CLIENT_IP'] || env['REMOTE_ADDR']) })
# IF no_tolerance, and request not safe?
if ((!!(ProjectHoneypot.configuration.no_tolerance) && !(request.safe?)) ||
# OR request score >= configuration score_tolerance
(request.score.to_i >= ProjectHoneypot.configuration.score_tolerance.to_i) ||
# OR request last_activity <= configuration score_tolerance
(!(request.last_activity.nil?) && request.last_activity.to_i <= ProjectHoneypot.configuration.last_activity_tolerance.to_i))
# THEN RETURN ERROR INSTEAD OF RESPONSE
error(env)
else
respond(env)
end
else
respond(env)
end
rescue
respond(env)
end
end

def error(env)
[:error_status_code, :error_headers, :error_message].map{ |k| ProjectHoneypot.configuration.send(k) }
end

def respond(env)
@status, @headers, @response = @app.call(env)
[@status, @headers, self]
end

def each(&block)
@response.each(&block)
end
end
end
10 changes: 10 additions & 0 deletions lib/project_honeypot/railtie.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
module ProjectHoneypot
class Railtie < Rails::Railtie
initializer "project_honeypot.configure_rails_initialization" do
Rails.application.middleware.use ProjectHoneypot::Middleware
end
generators do
require "generators/install_generator"
end
end if defined? ::Rails::Railtie
end
13 changes: 7 additions & 6 deletions project-honeypot.gemspec
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
Gem::Specification.new do |s|
s.name = %q{project-honeypot}
s.version = "0.1.3"
s.version = "0.1.4"
s.date = %q{2015-07-02}
s.authors = ["Charles Max Wood"]
s.email = %q{[email protected]}
s.summary = %q{Project-Honeypot provides a programatic interface to the Project Honeypot services.}
s.homepage = %q{http://teachmetocode.com/}
s.description = %q{Project-Honeypot provides a programatic interface to the Project Honeypot services. It can be used to identify spammers, bogus commenters, and harvesters. You will need a FREE api key from http://projecthoneypot.org}
s.add_dependency('net-dns2')
s.files = [ "README.rdoc",
"MIT-LICENSE",
"lib/project-honeypot.rb",
"lib/project_honeypot/url.rb",
"lib/project_honeypot/base.rb"]
s.add_dependency('test-unit')

s.files = `git ls-files`.split("\n")
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
s.require_paths = ["lib"]
end
6 changes: 3 additions & 3 deletions spec/base_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@

it "returns a Url object" do
url = @base.lookup("127.10.10.5")
url.should be_a ProjectHoneypot::Url
url.last_activity.should == 1
url.score.should == 63
expect(url).to be_a ProjectHoneypot::Url
expect(url.last_activity).to eq(1)
expect(url.score).to eq(63)
end

it "looks up non-ip addresses" do
Expand Down
47 changes: 47 additions & 0 deletions spec/middleware_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
require "spec_helper"

describe ProjectHoneypot::Middleware do
include Rack::Test::Methods

def app
Rack::Builder.new do
map '/' do
use ProjectHoneypot::Middleware
run Proc.new {|env| [200, {'Content-Type' => 'text/html'}, ['Hello World']] }
end
end.to_app
end

before(:all){
ENV['RACK_ENV'] = 'production'
@api_key = 'ABCD12345'
@ip = '1.2.3.4'
ProjectHoneypot.configure do |config|
config.api_key = @api_key
end
}

describe "good request" do
before(:each){
flexmock(Net::DNS::Resolver, :start => flexmock("answer", :answer => [nil]))
}

it 'says Hello World' do
post '/', {}, 'REMOTE_ADDR' => @ip
expect(last_response).to be_ok
expect(last_response.body).to eq('Hello World')
end
end

describe "bad request" do
before(:each){
flexmock(Net::DNS::Resolver, :start => flexmock("answer", :answer => ["somedomain.httpbl.org A Name 127.82.64.5"]))
}

it 'says WARNING' do
post '/', {}, 'REMOTE_ADDR' => @ip
expect(last_response).to_not be_ok
expect(last_response.body).to include('WARNING: This IP Address has been flagged for suspicious behavior. Be advised.')
end
end
end
5 changes: 3 additions & 2 deletions spec/spec_helper.rb
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
require "rubygems"
require "bundler/setup"
require "rspec"
require 'rack/test'
require "flexmock"
require File.dirname(__FILE__) + "/../lib/project_honeypot"
require "project-honeypot"

RSpec.configure do |config|
config.mock_with :flexmock
end
end
18 changes: 9 additions & 9 deletions spec/url_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,24 @@
end

it "is safe" do
@url.should_not be_safe
expect(@url).to_not be_safe
end

it "has the correct latest activity" do
@url.last_activity.should == 1
expect(@url.last_activity).to eq(1)
end

it "has the correct score" do
@url.score.should == 63
expect(@url.score).to eq(63)
end

it "has the correct offenses" do
@url.offenses.should include(:suspicious)
@url.offenses.should include(:harvester)
@url.offenses.should_not include(:comment_spammer)
@url.should be_suspicious
@url.should be_harvester
@url.should_not be_comment_spammer
expect(@url.offenses).to include(:suspicious)
expect(@url.offenses).to include(:harvester)
expect(@url.offenses).to_not include(:comment_spammer)
expect(@url).to be_suspicious
expect(@url).to be_harvester
expect(@url).to_not be_comment_spammer
end
end

Expand Down