first_click_free 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +97 -0
- data/Rakefile +30 -0
- data/config/domains.yml +195 -0
- data/lib/first_click_free.rb +42 -0
- data/lib/first_click_free/concerns/controller.rb +108 -0
- data/lib/first_click_free/exceptions/subsequent_access_exception.rb +6 -0
- data/lib/first_click_free/helpers/google.rb +41 -0
- data/lib/first_click_free/helpers/path.rb +22 -0
- data/lib/first_click_free/helpers/referrer.rb +26 -0
- data/lib/first_click_free/version.rb +3 -0
- data/lib/tasks/first_click_free_tasks.rake +24 -0
- data/spec/concerns/controller_spec.rb +128 -0
- data/spec/dummy/README.rdoc +28 -0
- data/spec/dummy/Rakefile +6 -0
- data/spec/dummy/app/assets/javascripts/application.js +13 -0
- data/spec/dummy/app/assets/stylesheets/application.css +13 -0
- data/spec/dummy/app/controllers/application_controller.rb +5 -0
- data/spec/dummy/app/helpers/application_helper.rb +2 -0
- data/spec/dummy/app/views/layouts/application.html.erb +14 -0
- data/spec/dummy/bin/bundle +3 -0
- data/spec/dummy/bin/rails +4 -0
- data/spec/dummy/bin/rake +4 -0
- data/spec/dummy/config.ru +4 -0
- data/spec/dummy/config/application.rb +23 -0
- data/spec/dummy/config/boot.rb +5 -0
- data/spec/dummy/config/database.yml +25 -0
- data/spec/dummy/config/environment.rb +5 -0
- data/spec/dummy/config/environments/development.rb +29 -0
- data/spec/dummy/config/environments/production.rb +80 -0
- data/spec/dummy/config/environments/test.rb +36 -0
- data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
- data/spec/dummy/config/initializers/inflections.rb +16 -0
- data/spec/dummy/config/initializers/mime_types.rb +5 -0
- data/spec/dummy/config/initializers/secret_token.rb +12 -0
- data/spec/dummy/config/initializers/session_store.rb +3 -0
- data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/spec/dummy/config/locales/en.yml +23 -0
- data/spec/dummy/config/routes.rb +56 -0
- data/spec/dummy/db/development.sqlite3 +0 -0
- data/spec/dummy/db/schema.rb +16 -0
- data/spec/dummy/db/test.sqlite3 +0 -0
- data/spec/dummy/log/development.log +18 -0
- data/spec/dummy/log/test.log +548 -0
- data/spec/dummy/public/404.html +58 -0
- data/spec/dummy/public/422.html +58 -0
- data/spec/dummy/public/500.html +57 -0
- data/spec/dummy/public/favicon.ico +0 -0
- data/spec/exceptions/subsequent_access_exception_spec.rb +5 -0
- data/spec/first_click_free_spec.rb +46 -0
- data/spec/helpers/google_spec.rb +104 -0
- data/spec/helpers/path_spec.rb +35 -0
- data/spec/helpers/referrer_spec.rb +39 -0
- data/spec/spec_helper.rb +18 -0
- metadata +185 -0
@@ -0,0 +1,41 @@
|
|
1
|
+
module FirstClickFree
|
2
|
+
module Helpers
|
3
|
+
module Google
|
4
|
+
|
5
|
+
# Public: Determine whether the agent requesting the resource
|
6
|
+
# is a Googlebot.
|
7
|
+
#
|
8
|
+
# This method operates by examining the User agent of the request.
|
9
|
+
# If the appropriate configuration is activated, it will also
|
10
|
+
# perform the necessary DNS requests to verify that the request
|
11
|
+
# originates from Google.
|
12
|
+
#
|
13
|
+
# perform_dns_lookup - Perform a DNS lookup to verify that the request
|
14
|
+
# is originating from Google.
|
15
|
+
#
|
16
|
+
# Returns true if a Googlebot has been identified, false if not.
|
17
|
+
def googlebot?(use_dns_lookup = true)
|
18
|
+
(FirstClickFree.test_mode && params.key?(:googlebot)) ||\
|
19
|
+
(request.user_agent == "Googlebot" && (use_dns_lookup ? verify_googlebot_domain : true))
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
# Private: Perform a reverse and then forward DNS lookup to
|
25
|
+
# verify that this request is coming from the correct domain
|
26
|
+
# (googlebot.com).
|
27
|
+
#
|
28
|
+
# Returns true if the hostname ends with googlebot.com and the
|
29
|
+
# forward DNS lookup IP address matches the request's remote IP address.
|
30
|
+
def verify_googlebot_domain
|
31
|
+
hostname = Resolv.getname(request.remote_ip)
|
32
|
+
# Structure of lookup return is:
|
33
|
+
# ["AF_INET", 80, "66.249.66.1", "66.249.66.1", 2, 2, 17]
|
34
|
+
ip = Socket.getaddrinfo(hostname, "http").first[2]
|
35
|
+
|
36
|
+
hostname =~ /.googlebot.com\Z/ && ip == request.remote_ip
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module FirstClickFree
|
2
|
+
module Helpers
|
3
|
+
module Path
|
4
|
+
|
5
|
+
require 'uri'
|
6
|
+
|
7
|
+
# Public: Determine if the requested path is allowed to bypass
|
8
|
+
# first click free.
|
9
|
+
#
|
10
|
+
# This method will check if the requested path is in a list of
|
11
|
+
# specifically known paths (see FirstClickFree.permitted_paths). By
|
12
|
+
# default the permitted paths list is empty so no special paths are
|
13
|
+
# allowed.
|
14
|
+
#
|
15
|
+
# Returns true if the requested path is specifically permitted, or
|
16
|
+
# false if it is not.
|
17
|
+
def permitted_path?
|
18
|
+
FirstClickFree.permitted_paths.include?(URI.parse(request.fullpath).path)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module FirstClickFree
|
2
|
+
module Helpers
|
3
|
+
module Referrer
|
4
|
+
|
5
|
+
require 'uri'
|
6
|
+
|
7
|
+
# Public: Determine if the request referrer is allowed to bypass
|
8
|
+
# first click free.
|
9
|
+
#
|
10
|
+
# By default, this method will check if the request referrer is in
|
11
|
+
# a list of known search engine domains (see config/domains.yml). If
|
12
|
+
# the domain is a search engine domain, then the request will bypass
|
13
|
+
# any present first click free, as most search engines require
|
14
|
+
# that first click free pages always be accessible from search listings.
|
15
|
+
#
|
16
|
+
# Returns true if the domain from the request referrer is permitted, or
|
17
|
+
# false if it is not.
|
18
|
+
def permitted_domain?
|
19
|
+
return true if FirstClickFree.test_mode && params.key?(:google_referrer)
|
20
|
+
request.try(:referrer) && FirstClickFree.permitted_domains.any? do |domain|
|
21
|
+
URI.parse(request.referrer).hostname =~ /#{domain}\Z/
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# desc "Explaining what the task does"
|
2
|
+
# task :first_click_free do
|
3
|
+
# # Task goes here
|
4
|
+
# end
|
5
|
+
|
6
|
+
require 'open-uri'
|
7
|
+
require 'yaml'
|
8
|
+
|
9
|
+
namespace :first_click_free do
|
10
|
+
|
11
|
+
desc "Update the list of domains permitted for first click free bypass
|
12
|
+
from Google's list of supported domains"
|
13
|
+
task :update_permitted_domains do
|
14
|
+
File.open(File.join(FirstClickFree.root, 'config', 'domains.yml'), 'wb') do |domain_file|
|
15
|
+
domains = open("http://www.google.com/supported_domains").read.split("\n")
|
16
|
+
domains << ".bing.com"
|
17
|
+
domains << ".yahoo.com"
|
18
|
+
|
19
|
+
domain_file.write domains.to_yaml
|
20
|
+
puts "Done. Wrote #{domains.length} domains to config/domains.yml"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
@@ -0,0 +1,128 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
require "ostruct"
|
3
|
+
|
4
|
+
Rails.application.routes.draw do
|
5
|
+
match "/first-click-free" => "anonymous#index", :as => "first_click_free", :via => "get"
|
6
|
+
end
|
7
|
+
|
8
|
+
|
9
|
+
describe FirstClickFree::Concerns::Controller, type: :controller do
|
10
|
+
|
11
|
+
let(:current_url) { "http://test.host/first-click-free" }
|
12
|
+
|
13
|
+
context "standard controller" do
|
14
|
+
controller do
|
15
|
+
allow_first_click_free
|
16
|
+
|
17
|
+
def index
|
18
|
+
head :ok
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# Redefined here for conciseness
|
23
|
+
def checksum(url)
|
24
|
+
Zlib.adler32(url).to_s
|
25
|
+
end
|
26
|
+
|
27
|
+
|
28
|
+
before { FirstClickFree.free_clicks = 1 }
|
29
|
+
|
30
|
+
context "first visit" do
|
31
|
+
before { get :index, test: true }
|
32
|
+
|
33
|
+
it { session[:first_click].should include checksum(current_url) }
|
34
|
+
it { request.env["first_click_free_count"].should eq 1 }
|
35
|
+
it { response.should be_success }
|
36
|
+
end
|
37
|
+
|
38
|
+
context "subsequent visit to same page" do
|
39
|
+
before { session[:first_click] = [ checksum(current_url) ] }
|
40
|
+
|
41
|
+
it { get :index; request.env["first_click_free_count"].should eq 1 }
|
42
|
+
it { expect { get :index }.not_to raise_error }
|
43
|
+
end
|
44
|
+
|
45
|
+
context "subsequent visit to different page" do
|
46
|
+
before { session[:first_click] = [ checksum("http://test.host/another-page") ] }
|
47
|
+
|
48
|
+
it { expect { get :index }.to raise_error FirstClickFree::Exceptions::SubsequentAccessException }
|
49
|
+
end
|
50
|
+
|
51
|
+
context "subsequent visit to different page with unused multiple clicks" do
|
52
|
+
before do
|
53
|
+
session[:first_click] = [ checksum("http://test.host/some-page"),
|
54
|
+
checksum("http://test.host/some-other-page") ]
|
55
|
+
FirstClickFree.free_clicks = 3
|
56
|
+
end
|
57
|
+
|
58
|
+
it { get :index; request.env["first_click_free_count"].should eq 3 }
|
59
|
+
it { expect { get :index }.not_to raise_error }
|
60
|
+
end
|
61
|
+
|
62
|
+
context "subsequent visit to different page with multiple clicks used up" do
|
63
|
+
before do
|
64
|
+
session[:first_click] = [ checksum("http://test.host/some-page"),
|
65
|
+
checksum("http://test.host/some-other-page"),
|
66
|
+
checksum("http://test.host/yet-another-page") ]
|
67
|
+
FirstClickFree.free_clicks = 3
|
68
|
+
end
|
69
|
+
|
70
|
+
it { expect { get :index }.to raise_error FirstClickFree::Exceptions::SubsequentAccessException }
|
71
|
+
end
|
72
|
+
|
73
|
+
context "googlebot visit" do
|
74
|
+
before { controller.stub(:googlebot? => true); get :index }
|
75
|
+
|
76
|
+
it { session[:first_click].should be_nil }
|
77
|
+
it { response.should be_success }
|
78
|
+
end
|
79
|
+
|
80
|
+
context "registered user vist" do
|
81
|
+
before { controller.stub(:user_for_first_click_free => true); get :index }
|
82
|
+
|
83
|
+
it { session[:first_click].should be_nil }
|
84
|
+
it { response.should be_success }
|
85
|
+
end
|
86
|
+
|
87
|
+
context "google referrer visit" do
|
88
|
+
before { get :index; controller.stub(permitted_domain?: true); get :index }
|
89
|
+
|
90
|
+
it { response.should be_success }
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
context "controller skipping an action" do
|
95
|
+
controller do
|
96
|
+
allow_first_click_free except: :index
|
97
|
+
|
98
|
+
def index
|
99
|
+
head :ok
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
before { get :index }
|
104
|
+
|
105
|
+
it { session[:first_click].should be_nil }
|
106
|
+
it { response.should be_success }
|
107
|
+
end
|
108
|
+
|
109
|
+
context "entire controller skipped" do
|
110
|
+
controller do
|
111
|
+
|
112
|
+
# Normally this would be done in ApplicationController or similar
|
113
|
+
allow_first_click_free
|
114
|
+
|
115
|
+
# And this would be done in a child controller
|
116
|
+
skip_first_click_free
|
117
|
+
|
118
|
+
def index
|
119
|
+
head :ok
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
before { get :index }
|
124
|
+
|
125
|
+
it { session[:first_click].should be_nil }
|
126
|
+
it { response.should be_success }
|
127
|
+
end
|
128
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
== README
|
2
|
+
|
3
|
+
This README would normally document whatever steps are necessary to get the
|
4
|
+
application up and running.
|
5
|
+
|
6
|
+
Things you may want to cover:
|
7
|
+
|
8
|
+
* Ruby version
|
9
|
+
|
10
|
+
* System dependencies
|
11
|
+
|
12
|
+
* Configuration
|
13
|
+
|
14
|
+
* Database creation
|
15
|
+
|
16
|
+
* Database initialization
|
17
|
+
|
18
|
+
* How to run the test suite
|
19
|
+
|
20
|
+
* Services (job queues, cache servers, search engines, etc.)
|
21
|
+
|
22
|
+
* Deployment instructions
|
23
|
+
|
24
|
+
* ...
|
25
|
+
|
26
|
+
|
27
|
+
Please feel free to use a different markup language if you do not plan to run
|
28
|
+
<tt>rake doc:app</tt>.
|
data/spec/dummy/Rakefile
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
// This is a manifest file that'll be compiled into application.js, which will include all the files
|
2
|
+
// listed below.
|
3
|
+
//
|
4
|
+
// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
|
5
|
+
// or vendor/assets/javascripts of plugins, if any, can be referenced here using a relative path.
|
6
|
+
//
|
7
|
+
// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
|
8
|
+
// compiled file.
|
9
|
+
//
|
10
|
+
// Read Sprockets README (https://github.com/sstephenson/sprockets#sprockets-directives) for details
|
11
|
+
// about supported directives.
|
12
|
+
//
|
13
|
+
//= require_tree .
|
@@ -0,0 +1,13 @@
|
|
1
|
+
/*
|
2
|
+
* This is a manifest file that'll be compiled into application.css, which will include all the files
|
3
|
+
* listed below.
|
4
|
+
*
|
5
|
+
* Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
|
6
|
+
* or vendor/assets/stylesheets of plugins, if any, can be referenced here using a relative path.
|
7
|
+
*
|
8
|
+
* You're free to add application-wide styles to this file and they'll appear at the top of the
|
9
|
+
* compiled file, but it's generally better to create a new file per style scope.
|
10
|
+
*
|
11
|
+
*= require_self
|
12
|
+
*= require_tree .
|
13
|
+
*/
|
@@ -0,0 +1,14 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html>
|
3
|
+
<head>
|
4
|
+
<title>Dummy</title>
|
5
|
+
<%= stylesheet_link_tag "application", media: "all", "data-turbolinks-track" => true %>
|
6
|
+
<%= javascript_include_tag "application", "data-turbolinks-track" => true %>
|
7
|
+
<%= csrf_meta_tags %>
|
8
|
+
</head>
|
9
|
+
<body>
|
10
|
+
|
11
|
+
<%= yield %>
|
12
|
+
|
13
|
+
</body>
|
14
|
+
</html>
|
data/spec/dummy/bin/rake
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
require File.expand_path('../boot', __FILE__)
|
2
|
+
|
3
|
+
require 'rails/all'
|
4
|
+
|
5
|
+
Bundler.require(*Rails.groups)
|
6
|
+
require "first_click_free"
|
7
|
+
|
8
|
+
module Dummy
|
9
|
+
class Application < Rails::Application
|
10
|
+
# Settings in config/environments/* take precedence over those specified here.
|
11
|
+
# Application configuration should go into files in config/initializers
|
12
|
+
# -- all .rb files in that directory are automatically loaded.
|
13
|
+
|
14
|
+
# Set Time.zone default to the specified zone and make Active Record auto-convert to this zone.
|
15
|
+
# Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC.
|
16
|
+
# config.time_zone = 'Central Time (US & Canada)'
|
17
|
+
|
18
|
+
# The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded.
|
19
|
+
# config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s]
|
20
|
+
# config.i18n.default_locale = :de
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# SQLite version 3.x
|
2
|
+
# gem install sqlite3
|
3
|
+
#
|
4
|
+
# Ensure the SQLite 3 gem is defined in your Gemfile
|
5
|
+
# gem 'sqlite3'
|
6
|
+
development:
|
7
|
+
adapter: sqlite3
|
8
|
+
database: db/development.sqlite3
|
9
|
+
pool: 5
|
10
|
+
timeout: 5000
|
11
|
+
|
12
|
+
# Warning: The database defined as "test" will be erased and
|
13
|
+
# re-generated from your development database when you run "rake".
|
14
|
+
# Do not set this db to the same as development or production.
|
15
|
+
test:
|
16
|
+
adapter: sqlite3
|
17
|
+
database: db/test.sqlite3
|
18
|
+
pool: 5
|
19
|
+
timeout: 5000
|
20
|
+
|
21
|
+
production:
|
22
|
+
adapter: sqlite3
|
23
|
+
database: db/production.sqlite3
|
24
|
+
pool: 5
|
25
|
+
timeout: 5000
|