├── .rspec ├── lib ├── rack │ ├── block │ │ ├── dsl │ │ │ ├── bot_ua_pattern.rb │ │ │ ├── builtin_bot_pattern.rb │ │ │ ├── matchers.rb │ │ │ └── responses.rb │ │ ├── version.rb │ │ ├── ipaddr │ │ │ └── monkeypatch.rb │ │ └── dsl.rb │ └── block.rb └── rack-block.rb ├── .gitignore ├── Gemfile ├── .travis.yml ├── Guardfile ├── spec ├── spec_helper.rb └── integrations │ ├── mock_app_helper.rb │ ├── normally_passes_access.rb │ ├── mock_app_works_spec.rb │ ├── bot_pattern_spec.rb │ ├── path_matching_spec.rb │ ├── ip_blocking_spec.rb │ ├── ua_blocking_spec.rb │ └── response_verbs_spec.rb ├── Rakefile ├── LICENSE ├── rack-block.gemspec ├── README.md └── index.html /.rspec: -------------------------------------------------------------------------------- 1 | -f documentation --color 2 | -------------------------------------------------------------------------------- /lib/rack/block/dsl/bot_ua_pattern.rb: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/rack-block.rb: -------------------------------------------------------------------------------- 1 | require "rack/block" 2 | 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | .bundle 3 | pkg/* 4 | *~ 5 | .rvmrc 6 | *\#* 7 | 8 | Gemfile.lock -------------------------------------------------------------------------------- /lib/rack/block/version.rb: -------------------------------------------------------------------------------- 1 | module Rack 2 | class Block 3 | VERSION = "0.2.0" 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | # Specify your gem's dependencies in rack-block.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /lib/rack/block/ipaddr/monkeypatch.rb: -------------------------------------------------------------------------------- 1 | require 'ipaddr' 2 | 3 | class IPAddr 4 | def =~(target) 5 | include?(target) 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # http://about.travis-ci.org/docs/user/build-configuration/ 2 | rvm: 3 | - 2.2.7 4 | - 2.3.4 5 | - 2.4.1 6 | notifications: 7 | email: false 8 | -------------------------------------------------------------------------------- /lib/rack/block/dsl.rb: -------------------------------------------------------------------------------- 1 | module Rack 2 | class Block 3 | module DSL 4 | end 5 | end 6 | end 7 | require 'rack/block/dsl/matchers' 8 | require 'rack/block/dsl/responses' 9 | -------------------------------------------------------------------------------- /Guardfile: -------------------------------------------------------------------------------- 1 | # A sample Guardfile 2 | # More info at https://github.com/guard/guard#readme 3 | 4 | guard 'rspec', :version => 2 do 5 | watch(%r{^spec/.+_spec\.rb$}) 6 | watch(%r{^lib/(.+)\.rb$}) { "spec" } 7 | watch('spec/spec_helper.rb') { "spec" } 8 | end 9 | 10 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) 2 | $LOAD_PATH.unshift(File.dirname(__FILE__)) 3 | require 'rspec' 4 | require 'rack/test' 5 | require 'rack-block' 6 | require 'mocha' 7 | 8 | # Requires supporting files with custom matchers and macros, etc, 9 | # in ./support/ and its subdirectories. 10 | Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f} 11 | 12 | RSpec.configure do |config| 13 | config.mock_framework = :mocha 14 | #... 15 | end 16 | 17 | -------------------------------------------------------------------------------- /spec/integrations/mock_app_helper.rb: -------------------------------------------------------------------------------- 1 | RSpec.configure do |config| 2 | config.include Rack::Test::Methods 3 | end 4 | 5 | DEFAULT_APP = lambda {|env| [200, {"Content-Type" => "text/plain"}, ["It is summer"]] } 6 | @app = nil 7 | def app 8 | @app || DEFAULT_APP 9 | end 10 | 11 | require 'rack/builder' 12 | def mock_app(&block) 13 | @app = Rack::Builder.new(&block) 14 | end 15 | 16 | if RUBY_VERSION < '1.9' 17 | require 'sinatra/base' 18 | class DummySinatra < Sinatra::Base 19 | get '/' do 20 | "This is a dummy access to: #{request.path_info}" 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/rack/block/dsl/builtin_bot_pattern.rb: -------------------------------------------------------------------------------- 1 | # patterns barrowed from CodeIgniter's helper 2 | # thanks: 3 | # https://github.com/EllisLab/CodeIgniter/blob/develop/application/config/user_agents.php#L200 4 | # http://www.useragentstring.com/pages/useragentstring.php 5 | # TODO more more bot sample 6 | module Rack::Block::DSL 7 | module Matchers 8 | BUILTIN_BOT_PATTERN = /#{ 9 | ["Googlebot", 10 | "MSNBot", 11 | "Bing", 12 | "Inktomi Slurp", 13 | "Yahoo", 14 | "AskJeeves", 15 | "FastCrawler", 16 | "InfoSeek Robot 1.0", 17 | "Baiduspider", 18 | "Lycos"].join("|") 19 | }/i 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | 3 | begin 4 | require 'rspec/core' 5 | require 'rspec/core/rake_task' 6 | RSpec::Core::RakeTask.new(:spec) do |spec| 7 | spec.pattern = FileList['spec/**/*_spec.rb'] 8 | spec.rspec_opts = "-f documentation --color" 9 | end 10 | 11 | RSpec::Core::RakeTask.new(:"spec:integrations") do |spec| 12 | spec.pattern = FileList['spec/integrations/**/*_spec.rb'] 13 | spec.rspec_opts = "-f documentation --color" 14 | end 15 | task :default => :spec 16 | rescue LoadError 17 | end 18 | 19 | desc "running pry console with rack-block gem and console helpers loaded" 20 | task :pry do 21 | $LOAD_PATH.unshift('./lib') 22 | require 'pry' 23 | require 'rack-block' 24 | Pry.start 25 | end 26 | -------------------------------------------------------------------------------- /spec/integrations/normally_passes_access.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + '/../spec_helper') 2 | require File.expand_path(File.dirname(__FILE__) + '/mock_app_helper') 3 | 4 | describe "Passing through accesses" do 5 | describe "Normal browser's access" do 6 | 7 | it "should pass the server response" do 8 | mock_app { 9 | use Rack::Block do 10 | bot_access do 11 | halt 404 12 | end 13 | end 14 | run DEFAULT_APP 15 | } 16 | 17 | header "User-Agent", "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0)" 18 | ['/', '/any', '/path/blocked'].each do |path| 19 | get path 20 | last_response.should be_ok 21 | last_response.body.should match /It is summer/ 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 Uchio Kondo 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this 4 | software and associated documentation files (the "Software"), to deal in the Software 5 | without restriction, including without limitation the rights to use, copy, modify, merge, 6 | publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons 7 | to whom the Software is furnished to do so, subject to the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be included in all copies or 10 | substantial portions of the Software. 11 | 12 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING 13 | BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 14 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 15 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 16 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | 18 | http://mit-license.org/ -------------------------------------------------------------------------------- /spec/integrations/mock_app_works_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + '/../spec_helper') 2 | require File.expand_path(File.dirname(__FILE__) + '/mock_app_helper') 3 | 4 | describe "There is no problem that the #mock_app helper works" do 5 | before do 6 | mock_app do 7 | map '/nil' do 8 | run lambda {|env| [404, {"Content-Type" => "text/plain"}, ["It is no summer"]] } 9 | end 10 | 11 | map '/auth' do 12 | use Rack::Auth::Basic, "fobbiden" do |u, p| false end 13 | run lambda {|env| [200, {"Content-Type" => "text/plain"}, ["Not reachable"]] } 14 | end 15 | 16 | map '/' do 17 | run lambda {|env| [200, {"Content-Type" => "text/plain"}, ["It is summer"]] } 18 | end 19 | end 20 | end 21 | 22 | it "works on a normal access" do 23 | lambda { get '/' }.should_not raise_error 24 | last_response.should be_ok 25 | end 26 | 27 | it "works when not found" do 28 | get '/nil' 29 | last_response.should be_not_found 30 | end 31 | 32 | it "works using rack middleware" do 33 | get '/auth' 34 | last_response.status.should eq 401 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /rack-block.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | $:.push File.expand_path("../lib", __FILE__) 3 | require "rack/block/version" 4 | 5 | Gem::Specification.new do |s| 6 | s.name = "rack-block" 7 | s.version = Rack::Block::VERSION 8 | s.authors = ["Uchio Kondo"] 9 | s.email = ["udzura@udzura.jp"] 10 | s.homepage = "http://udzura.jp/rack-block" 11 | s.summary = %q{A rack middleware for controlling accesses by search bot or not, remote ip address, etc.} 12 | s.description = %q{A rack middleware for controlling accesses by search bot or not, remote ip address, etc.} 13 | 14 | s.rubyforge_project = "rack-block" 15 | 16 | s.files = `git ls-files`.split("\n") 17 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 18 | s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } 19 | s.require_paths = ["lib"] 20 | 21 | s.add_runtime_dependency "rack", '>= 1.3' 22 | 23 | #s.add_development_dependency "bundler", '~> 1.1.rc' 24 | s.add_development_dependency "pry" 25 | s.add_development_dependency "rake", '> 0' 26 | s.add_development_dependency "rspec", '>= 2' 27 | s.add_development_dependency "mocha", '>= 0' 28 | s.add_development_dependency "rack-test", '> 0' 29 | s.add_development_dependency "sinatra", '> 1.0' 30 | s.add_development_dependency "guard-rspec" 31 | end 32 | -------------------------------------------------------------------------------- /spec/integrations/bot_pattern_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + '/../spec_helper') 2 | require File.expand_path(File.dirname(__FILE__) + '/mock_app_helper') 3 | 4 | describe "Blocking search bots" do 5 | def access_root_as(ua) 6 | header "User-Agent", ua 7 | get '/' 8 | end 9 | 10 | before do 11 | mock_app do 12 | use Rack::Block do 13 | bot_access { halt 404 } 14 | end 15 | run DEFAULT_APP 16 | end 17 | end 18 | 19 | it 'blocks google bot' do 20 | access_root_as 'Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)' 21 | last_response.should be_not_found 22 | end 23 | 24 | it 'blocks msn bot' do 25 | access_root_as 'msnbot/1.1 (+http://search.msn.com/msnbot.htm)' 26 | last_response.should be_not_found 27 | end 28 | 29 | it 'blocks bing bot' do 30 | access_root_as 'Mozilla/5.0 (compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm)' 31 | last_response.should be_not_found 32 | end 33 | 34 | it 'blocks yahoo! slurp' do 35 | access_root_as 'Mozilla/5.0 (compatible; Yahoo! Slurp; http://help.yahoo.com/help/us/ysearch/slurp)' 36 | last_response.should be_not_found 37 | end 38 | 39 | it 'blocks baiduspider' do 40 | access_root_as 'Baiduspider+(+http://www.baidu.com/search/spider_jp.html)' 41 | last_response.should be_not_found 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /spec/integrations/path_matching_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + '/../spec_helper') 2 | require File.expand_path(File.dirname(__FILE__) + '/mock_app_helper') 3 | 4 | describe "Path matching patterns" do 5 | it 'matching path should specified with `/\' starting' do 6 | mock_app { 7 | use Rack::Block do 8 | bot_access { path('/foo'){ halt 404 } } 9 | end 10 | run DEFAULT_APP 11 | } 12 | 13 | header "User-Agent", "Googlebot" 14 | get '/foo' 15 | last_response.should be_not_found 16 | 17 | get '/bar/foo' 18 | last_response.should be_ok 19 | end 20 | 21 | it 'matching glob `*\' should be a wildcard' do 22 | mock_app { 23 | use Rack::Block do 24 | bot_access { path('/foo/*/bar'){ halt 500 } } 25 | end 26 | run DEFAULT_APP 27 | } 28 | 29 | header "User-Agent", "Googlebot" 30 | get '/foo/123/bar' 31 | last_response.should be_server_error 32 | 33 | get '/foo/hogehoge/bar/4' 34 | last_response.should be_server_error 35 | end 36 | 37 | it 'path including `.\' should be sane' do 38 | mock_app { 39 | use Rack::Block do 40 | bot_access { path('/favicon.ico'){ halt 404 } } 41 | end 42 | run DEFAULT_APP 43 | } 44 | 45 | header "User-Agent", "Googlebot" 46 | get '/favicon.ico' 47 | last_response.should be_not_found 48 | 49 | header "User-Agent", "Googlebot" 50 | get '/favicon_ico' 51 | last_response.should be_ok 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /lib/rack/block.rb: -------------------------------------------------------------------------------- 1 | require 'rack' 2 | module Rack 3 | class Block 4 | require 'rack/block/dsl' 5 | 6 | include DSL::Matchers 7 | include DSL::Responses 8 | 9 | def initialize(app, &b) 10 | @_current_matching_type = nil 11 | @_current_matching_ua_pattern = nil 12 | @_current_matching_ip_pattern = nil 13 | @_current_matching_path = nil 14 | @ua_matchers = {} 15 | @ip_matchers = {} 16 | instance_eval(&b) 17 | @app = app 18 | end 19 | 20 | attr_reader :app 21 | attr_accessor :ua_matchers, :ip_matchers 22 | 23 | def call(env) 24 | req = Rack::Request.new(env) 25 | self.ua_matchers.each_pair do |pattern, path_matchers| 26 | if pattern =~ req.user_agent 27 | path_matchers.each_pair do |path, action_with_args| 28 | if path =~ req.path_info 29 | action, *args = action_with_args 30 | return send action, req, *args 31 | end 32 | end 33 | end 34 | end 35 | 36 | self.ip_matchers.each_pair do |pattern, path_matchers| 37 | if pattern =~ req.ip 38 | path_matchers.each_pair do |path, action_with_args| 39 | if path =~ req.path_info 40 | action, *args = action_with_args 41 | return send action, req, *args 42 | end 43 | end 44 | end 45 | end 46 | 47 | app.call(env) 48 | end 49 | # Your code goes here... 50 | end 51 | end 52 | 53 | require "rack/block/version" 54 | -------------------------------------------------------------------------------- /lib/rack/block/dsl/matchers.rb: -------------------------------------------------------------------------------- 1 | require 'rack/block/dsl/builtin_bot_pattern' 2 | require 'rack/block/ipaddr/monkeypatch' 3 | module Rack::Block::DSL 4 | module Matchers 5 | def bot_access(&block) 6 | ua_pattern BUILTIN_BOT_PATTERN, &block 7 | end 8 | 9 | # TODO it's NO DRY 10 | def ua_pattern(pattern, &block) 11 | @_current_matching_type, orig = :by_UA, @_current_matching_type 12 | @_current_matching_ua_pattern, orig_ptn = pattern, @_current_matching_ua_pattern 13 | yield 14 | ensure 15 | @_current_matching_ua_pattern = orig_ptn 16 | @_current_matching_type = orig 17 | end 18 | 19 | def ip_pattern(pattern, &block) 20 | @_current_matching_type, orig = :by_IP, @_current_matching_type 21 | @_current_matching_ip_pattern, orig_ptn = ip_to_pattern(pattern), @_current_matching_ip_pattern 22 | yield 23 | ensure 24 | @_current_matching_ip_pattern = orig_ptn 25 | @_current_matching_type = orig 26 | end 27 | 28 | alias block_ua ua_pattern 29 | alias block_ip ip_pattern 30 | 31 | def path(pattern, &block) 32 | @_current_matching_path, orig = pattern, @_current_matching_path 33 | yield 34 | ensure 35 | @_current_matching_path = orig 36 | end 37 | 38 | private 39 | def ip_to_pattern(ip_pattern) 40 | case ip_pattern 41 | when /^(\d+)(\.\d+){3}$/ 42 | Regexp.compile("^" + ip_pattern.gsub('.', '\\.') + "$") 43 | when /^(\d+)(\.\d+){0,2}\.?$/ 44 | ip_pattern = ip_pattern.sub(/\.$/, '') 45 | Regexp.compile("^" + ip_pattern.gsub('.', '\\.') + '(\\.\\d+)+' + "$") 46 | when /^\d+\.\d+\.\d+\.\d+\/\d+$/ 47 | IPAddr.new(ip_pattern) 48 | else 49 | raise ArgumentError, 'passed invalid IP string' 50 | end 51 | end 52 | 53 | def in_ua_block? 54 | @_current_matching_type == :by_UA 55 | end 56 | 57 | def in_ip_block? 58 | @_current_matching_type == :by_IP 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /spec/integrations/ip_blocking_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + '/../spec_helper') 2 | require File.expand_path(File.dirname(__FILE__) + '/mock_app_helper') 3 | 4 | describe "Blocking by IP" do 5 | before do 6 | Rack::Request.any_instance.stubs(:ip).returns('10.20.30.40') 7 | end 8 | 9 | it 'blocks accesses from a specific IP' do 10 | mock_app { 11 | use Rack::Block do 12 | ip_pattern '10.20.30.40' do 13 | halt 404 14 | end 15 | end 16 | run DEFAULT_APP 17 | } 18 | 19 | ['/', '/any', '/path/blocked'].each do |path| 20 | get path 21 | last_response.should be_not_found 22 | end 23 | end 24 | 25 | describe 'blocks accesses from a specific IP pattern' do 26 | before do 27 | mock_app { 28 | use Rack::Block do 29 | ip_pattern '10.20.30.' do 30 | halt 404 31 | end 32 | end 33 | run DEFAULT_APP 34 | } 35 | end 36 | 37 | it 'blocks 10.20.30.40 when supplied with 10.20.30.' do 38 | ['/', '/any', '/path/blocked'].each do |path| 39 | get path 40 | last_response.should be_not_found 41 | end 42 | end 43 | 44 | it 'blocks 10.20.30.50 when supplied with 10.20.30.' do 45 | Rack::Request.any_instance.stubs(:ip).returns('10.20.30.50') 46 | ['/', '/any', '/path/blocked'].each do |path| 47 | get path 48 | last_response.should be_not_found 49 | end 50 | end 51 | end 52 | 53 | describe 'blocks accesses from a specific IP pattern(with a netmask)' do 54 | before do 55 | mock_app { 56 | use Rack::Block do 57 | ip_pattern '10.20.30.0/24' do 58 | halt 404 59 | end 60 | end 61 | run DEFAULT_APP 62 | } 63 | end 64 | 65 | it 'blocks 10.20.30.40 when supplied with 10.20.30.0/24' do 66 | Rack::Request.any_instance.stubs(:ip).returns('10.20.30.40') 67 | ['/', '/any', '/path/blocked'].each do |path| 68 | get path 69 | last_response.should be_not_found 70 | end 71 | end 72 | 73 | it 'does not block 10.20.31.0 when supplied with 10.20.30.0/24' do 74 | Rack::Request.any_instance.stubs(:ip).returns('10.20.31.0') 75 | ['/', '/any', '/path/blocked'].each do |path| 76 | get path 77 | last_response.should be_ok 78 | end 79 | end 80 | end 81 | end 82 | -------------------------------------------------------------------------------- /spec/integrations/ua_blocking_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + '/../spec_helper') 2 | require File.expand_path(File.dirname(__FILE__) + '/mock_app_helper') 3 | 4 | describe "Blocking by UA" do 5 | describe "Blocking built-in bot UA pattern" do 6 | 7 | it "blocks all bot access if no nesting block" do 8 | mock_app { 9 | use Rack::Block do 10 | bot_access do 11 | halt 404 12 | end 13 | end 14 | run DEFAULT_APP 15 | } 16 | 17 | header "User-Agent", "Googlebot" 18 | ['/', '/any', '/path/blocked'].each do |path| 19 | get path 20 | last_response.should be_not_found 21 | end 22 | end 23 | 24 | it "blocks specific paths" do 25 | mock_app { 26 | use Rack::Block do 27 | bot_access do 28 | path '/foo' do 29 | halt 404 30 | end 31 | end 32 | end 33 | run DEFAULT_APP 34 | } 35 | 36 | header "User-Agent", "Googlebot" 37 | get '/foo' 38 | last_response.should be_not_found 39 | end 40 | 41 | it "does not block excepting specified paths" do 42 | mock_app { 43 | use Rack::Block do 44 | bot_access do 45 | path '/foo' do 46 | halt 404 47 | end 48 | end 49 | end 50 | run DEFAULT_APP 51 | } 52 | 53 | header "User-Agent", "Googlebot" 54 | get '/' 55 | last_response.should be_ok 56 | end 57 | end 58 | 59 | describe "Blocking customized UA pattern" do 60 | before do 61 | mock_app { 62 | use Rack::Block do 63 | ua_pattern /MSIE [678]\.[05].*Windows/ do 64 | halt 404 65 | end 66 | end 67 | run DEFAULT_APP 68 | } 69 | end 70 | 71 | it "blocks customized UA" do 72 | header "User-Agent", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1)" 73 | ['/', '/any', '/path/blocked'].each do |path| 74 | get path 75 | last_response.should be_not_found 76 | end 77 | end 78 | 79 | it "doesn't block non-matching UA" do 80 | header "User-Agent", "Mozilla/5.0 (Ubuntu; X11; Linux i686; rv:8.0) Gecko/20100101 Firefox/8.0" 81 | ['/', '/any', '/path/not-blocked'].each do |path| 82 | get path 83 | last_response.should be_ok 84 | end 85 | end 86 | end 87 | end 88 | -------------------------------------------------------------------------------- /lib/rack/block/dsl/responses.rb: -------------------------------------------------------------------------------- 1 | module Rack::Block::DSL 2 | module Responses 3 | def detect_matching_pattern 4 | if in_ua_block? 5 | ua_matchers[@_current_matching_ua_pattern] ||= {} 6 | elsif in_ip_block? 7 | ip_matchers[@_current_matching_ip_pattern] ||= {} 8 | end 9 | end 10 | 11 | # TODO non DRY on handling `@_current_matching_path' !!!! it is terrible!! 12 | def halt(code, opts={}) 13 | if @_current_matching_path 14 | path = Regexp.compile("^" + @_current_matching_path.gsub(/\./, '\\.').gsub(/\*/, '.*')) 15 | current_matchers = detect_matching_pattern 16 | current_matchers[path] = [:do_halt, code, opts] 17 | else 18 | path '*' do 19 | halt code, opts 20 | end 21 | end 22 | end 23 | 24 | def redirect(dest_path, opts={}) 25 | if @_current_matching_path 26 | path = Regexp.compile("^" + @_current_matching_path.gsub(/\./, '\\.').gsub(/\*/, '.*')) 27 | current_matchers = detect_matching_pattern 28 | current_matchers[path] = [:do_redirect, dest_path, opts] 29 | else 30 | path '*' do 31 | redirect dest_path, opts 32 | end 33 | end 34 | end 35 | 36 | def dummy_app(opts={}, &app_proc) 37 | if @_current_matching_path 38 | path = Regexp.compile("^" + @_current_matching_path.gsub(/\./, '\\.').gsub(/\*/, '.*')) 39 | current_matchers = detect_matching_pattern 40 | current_matchers[path] = [:do_dummy_app, app_proc, opts] 41 | else 42 | path '*' do 43 | dummy_app opts, &app_proc 44 | end 45 | end 46 | end 47 | 48 | alias_method :double, :dummy_app 49 | 50 | private 51 | def do_halt(req, code, opts={}) 52 | headers = {"Content-Type" => "text/plain"}.merge(opts[:headers] || {}) 53 | body = opts[:body] || "Halt!" 54 | return [code, headers, [body]] 55 | end 56 | 57 | def do_redirect(req, dest_path, opts={}) 58 | dest_full_path = case dest_path 59 | when /^https?:\/\// 60 | dest_path 61 | else 62 | uri = URI.parse req.url 63 | uri.path = dest_path 64 | uri.to_s 65 | end 66 | code = opts[:status_code] || 301 67 | return [code, {"Location" => dest_full_path}, []] 68 | end 69 | 70 | def do_dummy_app(req, app_proc, opts={}) 71 | case app_proc.arity 72 | when -1, 0 73 | return app_proc.call.call(req.env) 74 | else 75 | return app_proc.call(req.env) 76 | end 77 | end 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rack-block 2 | 3 | A rack middleware for controlling accesses by search bot or not, remote ip address, etc. 4 | 5 | ## Install 6 | 7 | ```bash 8 | $ gem install rack-block 9 | ``` 10 | 11 | No doubt it depends on `rack` (>= 1.3). 12 | 13 | ## Usage 14 | 15 | ### block all bot accesses: 16 | 17 | ```ruby 18 | use Rack::Block do 19 | bot_access do 20 | halt 404 21 | end 22 | end 23 | run App.new 24 | ``` 25 | 26 | ### block all bot accesses on a specific path: 27 | 28 | ```ruby 29 | use Rack::Block do 30 | bot_access do 31 | path '/secret/*' do 32 | halt 404 33 | end 34 | end 35 | end 36 | run App.new 37 | ``` 38 | 39 | ### block some patterns of accesses: 40 | 41 | ```ruby 42 | use Rack::Block do 43 | ua_pattern /googlebot/i do 44 | halt 404 45 | end 46 | end 47 | run App.new 48 | ``` 49 | 50 | ### block accesses from specific IP(s): 51 | 52 | ```ruby 53 | use Rack::Block do 54 | ip_pattern '192.0.0.0' do 55 | # expressions like '192.0.0.' also available 56 | halt 404 57 | end 58 | end 59 | run App.new 60 | ``` 61 | 62 | ### redirect accesses: 63 | 64 | ```ruby 65 | use Rack::Block do 66 | bot_access do 67 | path '/secret/*' do 68 | redirect '/' 69 | end 70 | end 71 | end 72 | run App.new 73 | ``` 74 | 75 | ### redirect accesses to a double app: 76 | 77 | ```ruby 78 | use Rack::Block do 79 | bot_access do 80 | path '/secret/*' do 81 | # TheDummy is a Rack-compatible app 82 | double { TheDummy.new } 83 | end 84 | end 85 | end 86 | run App.new 87 | ``` 88 | 89 | More usage on RDoc: [http://rubydoc.info/github/udzura/rack-block/master/frames] 90 | 91 | Or please look into `spec/*` 92 | 93 | ## Related Sites 94 | 95 | * (official)[http://udzura.jp/rack-block] 96 | * (rubygems.org)[https://rubygems.org/gems/rack-block] 97 | * (github)[https://github.com/udzura/rack-block] 98 | * (travis-ci)[http://travis-ci.org/udzura/rack-block] / build status 99 | * (gemnagium)[https://gemnasium.com/udzura/rack-block] / dep status 100 | * (author's blog)[http://blog.udzura.jp] (Japanese) 101 | 102 | ## Todo 103 | 104 | * Make it more DRY 105 | * More test cases 106 | * Refactoring internal classes 107 | * Proxying accesses to another server 108 | * Passing IP patterns like `'192.0.0.0/24'`... 109 | 110 | ## Contributing to rack-block 111 | 112 | * Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet 113 | * Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it 114 | * Fork the project 115 | * Start a feature/bugfix branch 116 | * Commit and push until you are happy with your contribution 117 | * Make sure to add tests for it. This is important so I don't break it in a future version unintentionally. 118 | * Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it. 119 | 120 | ## Copyright 121 | 122 | Copyright (c) 2011 Uchio Kondo . See LICENSE for 123 | further details. 124 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | udzura/rack-block @ GitHub 7 | 8 | 31 | 32 | 33 | 34 | Fork me on GitHub 35 | 36 |
37 | 38 |
39 | 40 | 41 | 42 | 43 |
44 | 45 |

rack-block 46 | by udzura

47 | 48 |
49 | A rack middleware for handling search bot access, ip block, etc. 50 |
51 | 52 | 53 | 54 | 55 |

Dependencies

56 |

rack (>= 1.3)

57 | 58 | 59 | 60 |

Install

61 |

gem install rack-block 62 | 63 | then edit `config.ru`: 64 | use Rack::Block do 65 | bot_access do 66 | path '/foo' do 67 | halt 404 68 | end 69 | end 70 | end 71 | run YourApp.new 72 |

73 | 74 | 75 | 76 |

License

77 |

MIT License.

78 | 79 | 80 | 81 |

Authors

82 |

Uchio Kondo (udzura@udzura.jp)

83 | 84 | 85 | 86 |

Contact

87 |

Uchio Kondo (udzura@udzura.jp)

88 | 89 | 90 |

Download

91 |

92 | You can download this project in either 93 | zip or 94 | tar formats. 95 |

96 |

You can also clone the project with Git 97 | by running: 98 |

$ git clone git://github.com/udzura/rack-block
99 |

100 | 101 | 104 | 105 |
106 | 107 | 108 | 109 | -------------------------------------------------------------------------------- /spec/integrations/response_verbs_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + '/../spec_helper') 2 | require File.expand_path(File.dirname(__FILE__) + '/mock_app_helper') 3 | 4 | describe "Response value verbs" do 5 | before do 6 | end 7 | 8 | it 'can halt with status code 404' do 9 | mock_app { 10 | use Rack::Block do 11 | bot_access { path('*'){ halt 404 } } 12 | end 13 | run DEFAULT_APP 14 | } 15 | 16 | header "User-Agent", "Googlebot" 17 | get '/' 18 | last_response.should be_not_found 19 | end 20 | 21 | it 'can halt with status code 500' do 22 | mock_app { 23 | use Rack::Block do 24 | bot_access { path('*'){ halt 500 } } 25 | end 26 | run DEFAULT_APP 27 | } 28 | 29 | header "User-Agent", "Googlebot" 30 | get '/' 31 | last_response.status.should eq(500) 32 | end 33 | 34 | it 'can redirect' do 35 | mock_app { 36 | use Rack::Block do 37 | bot_access { path('*'){ redirect 'http://www.google.com/' } } 38 | end 39 | run DEFAULT_APP 40 | } 41 | 42 | header "User-Agent", "Googlebot" 43 | get '/' 44 | last_response.should be_redirect 45 | end 46 | 47 | it 'can redirect internal' do 48 | mock_app { 49 | use Rack::Block do 50 | bot_access { path('/foo/*'){ redirect '/' } } 51 | end 52 | run DEFAULT_APP 53 | } 54 | 55 | header "User-Agent", "Googlebot" 56 | get '/foo/bar' 57 | last_response.should be_redirect 58 | 59 | follow_redirect! 60 | last_response.body.should match /It is summer/ 61 | end 62 | 63 | context 'doubling application' do 64 | require 'sinatra/base' 65 | if RUBY_VERSION >= '1.9' 66 | let :dummy do 67 | Class.new(Sinatra::Base) do 68 | get '/' do 69 | "This is a dummy access to: #{request.path_info}" 70 | end 71 | end 72 | end 73 | else 74 | let(:dummy) { DummySinatra } 75 | end 76 | 77 | it 'can point access to a dummy application' do 78 | dummy = dummy() 79 | mock_app { 80 | use Rack::Block do 81 | bot_access do 82 | path('*') do 83 | dummy_app { dummy.new } 84 | end 85 | end 86 | end 87 | run DEFAULT_APP 88 | } 89 | 90 | header "User-Agent", "Googlebot" 91 | get '/' 92 | last_response.body.should match /This is a dummy access to: \// 93 | 94 | get '/not-found' 95 | last_response.should be_not_found 96 | end 97 | 98 | it 'should be called with env' do 99 | dummy = dummy() 100 | mock_app { 101 | use Rack::Block do 102 | bot_access do 103 | path('*') do 104 | double do |env| 105 | # Sinatra::Base and its subclasses have a #call method in his class methods 106 | dummy.call(env) 107 | end 108 | end 109 | end 110 | end 111 | run DEFAULT_APP 112 | } 113 | 114 | header "User-Agent", "Googlebot" 115 | get '/' 116 | last_response.body.should match /This is a dummy access to: \// 117 | end 118 | 119 | it 'should be aliased by #double' do 120 | dummy = dummy() 121 | mock_app { 122 | use Rack::Block do 123 | bot_access do 124 | path('*') do 125 | double { dummy.new } 126 | end 127 | end 128 | end 129 | run DEFAULT_APP 130 | } 131 | 132 | header "User-Agent", "Googlebot" 133 | get '/' 134 | last_response.body.should match /This is a dummy access to: \// 135 | end 136 | end 137 | end 138 | --------------------------------------------------------------------------------