snapsearch-client-ruby 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,15 @@
1
+ # Notes to run:
2
+ # gem install bundler
3
+ # bundle install
4
+ # rackup
5
+ #
6
+ # Testing:
7
+ # Visit http://localhost:9292/
8
+ # Visit http://localhost:9292/?_escaped_fragment_
9
+
10
+ require 'bundler/setup'
11
+ require 'pathname'
12
+ $:.unshift( Pathname.new(__FILE__).join('..', 'lib').expand_path.to_s )
13
+ require 'sinatra_snap_search'
14
+
15
+ run SinatraSnapSearch
@@ -0,0 +1,19 @@
1
+ require 'bundler/setup'
2
+ require 'sinatra/base'
3
+ require 'pathname'
4
+ require 'snap_search'
5
+
6
+ class SinatraSnapSearch < Sinatra::Base
7
+
8
+ configure do
9
+ set :root, SnapSearch.root.join('examples', 'sinatra')
10
+ enable :sessions, :logging, :method_override, :static
11
+
12
+ use Rack::SnapSearch, email: 'email', key: 'key'
13
+ end
14
+
15
+ get '/' do
16
+ redirect to('/index.html')
17
+ end
18
+
19
+ end
@@ -0,0 +1,15 @@
1
+ <!DOCTYPE html>
2
+ <html lang='en'>
3
+ <head>
4
+ <meta charset='utf-8'>
5
+ <title>SnapSearch Example</title>
6
+ <link href="/css/blah.css" media="all" rel="stylesheet" />
7
+ <!--[if lt IE 9]>
8
+ <script src="http://html5shim.googlecode.com/svn/trunk/html5.js"></script>
9
+ <![endif]-->
10
+ <!-- <script src="/js/blah.js"></script> -->
11
+ </head>
12
+ <body>
13
+ <h1>SnapSearch Example</h1>
14
+ </body>
15
+ </html>
@@ -0,0 +1,143 @@
1
+ require 'snap_search'
2
+ require 'rack/snap_search/config'
3
+
4
+ module Rack
5
+
6
+ # Use to enable SnapSearch detection within your web application.
7
+ class SnapSearch
8
+
9
+ # Initialize the middleware.
10
+ #
11
+ # @param [#call] app The Rack application.
12
+ # @param [Hash, #to_h] options Options to configure this middleware with.
13
+ # @yield [Rack::SnapSearch::Config, SnapSearch::Detector] A block to further modify the middleware.
14
+ # @yieldparam [Rack::SnapSearch::Config] config Options to configure this middleware with, optionally preset with the `options` param.
15
+ # @yieldparam [SnapSearch::Detector] detector The instance of the Detector class which will be used for detecting whether requests are coming from a robot.
16
+ def initialize(app, options={}, &block)
17
+ raise TypeError, 'app must respond to #call' unless app.respond_to?(:call)
18
+ raise TypeError, 'options must be a Hash or respond to #to_h or #to_hash' unless options.is_a?(Hash) || options.respond_to?(:to_h) || options.respond_to?(:to_hash)
19
+ options = options.to_h rescue options.to_hash
20
+
21
+ @app = app
22
+
23
+ setup_config(options)
24
+
25
+ block.call(@config) if block_given?
26
+
27
+ setup_client
28
+ setup_detector
29
+ setup_interceptor
30
+ end
31
+
32
+ # Run the middleware.
33
+ #
34
+ # @param [Hash, to_h] app The Rack environment
35
+ def call(environment)
36
+ raise TypeError, 'environment must be a Hash or respond to #to_h or #to_hash' unless environment.is_a?(Hash) || environment.respond_to?(:to_h) || environment.respond_to?(:to_hash)
37
+ environment = environment.to_h rescue environment.to_hash
38
+
39
+ setup_x_forwarded_proto(environment) if @config.x_forwarded_proto
40
+
41
+ setup_api_response_from_environment(environment) # Will set @api_response if one is given from the API
42
+
43
+ if @api_response
44
+ setup_attributes_from_api_response
45
+ else
46
+ setup_attributes_from_app(environment)
47
+ end
48
+
49
+ @config.response_callback.nil? ? rack_response : @config.response_callback.call(*rack_response)
50
+ end
51
+
52
+ protected
53
+
54
+ # == Initialization Helpers
55
+
56
+ # Setup the Config instance from the given options.
57
+ def setup_config(options)
58
+ @config = Rack::SnapSearch::Config.new(options)
59
+
60
+ @config.x_forwarded_proto ||= true
61
+ end
62
+
63
+ # Setup the Client instance from the @config.
64
+ def setup_client
65
+ @client = ::SnapSearch::Client.new(
66
+ email: @config.email,
67
+ key: @config.key,
68
+ parameters: @config.parameters,
69
+ api_url: @config.api_url,
70
+ ca_cert_file: @config.ca_cert_file
71
+ )
72
+ end
73
+
74
+ # Setup the Detector instance from the @config.
75
+ def setup_detector
76
+ @detector = ::SnapSearch::Detector.new(
77
+ matched_routes: @config.matched_routes,
78
+ ignored_routes: @config.ignored_routes,
79
+ robots_json: @config.robots_json,
80
+ extensions_json: @config.extensions_json,
81
+ check_static_files: @config.check_static_files
82
+ )
83
+ end
84
+
85
+ # Setup the Interceptor instance using the @client and @detector, then setup callbacks if needed.
86
+ def setup_interceptor
87
+ @interceptor = ::SnapSearch::Interceptor.new(@client, @detector)
88
+
89
+ @interceptor.before_intercept(&@config.before_intercept) unless @config.before_intercept.nil?
90
+ @interceptor.after_intercept(&@config.after_intercept) unless @config.before_intercept.nil?
91
+ end
92
+
93
+ # == Running Helpers
94
+
95
+ # Alter the environment if the X-FORWARDED-PROTO header is given.
96
+ def setup_x_forwarded_proto(environment)
97
+ # Check X-Forwarded-Proto because Heroku SSL Support terminates at the load balancer
98
+ if environment['X-FORWARDED-PROTO']
99
+ environment['HTTPS'] = true && environment['rack.url_scheme'] = 'https' && environment['SERVER_PORT'] = 443 if environment['X-FORWARDED-PROTO'] == 'https'
100
+ environment['HTTPS'] = false && environment['rack.url_scheme'] = 'http' && environment['SERVER_PORT'] = 80 if env['X-FORWARDED-PROTO'] == 'http'
101
+ end
102
+ end
103
+
104
+ # Intercept and return the response.
105
+ def setup_api_response_from_environment(environment)
106
+ begin
107
+ request = Rack::Request.new(environment.to_h)
108
+ @api_response = @interceptor.intercept(request: request)
109
+ rescue SnapSearch::Exception => exception
110
+ @config.on_exception.nil? ? raise(exception) : @config.on_exception.call(exception)
111
+ end
112
+ end
113
+
114
+ # Setup the Location header in the response.
115
+ def setup_location_header
116
+ response_location_header = @api_response['headers'].find { |header| header['name'] == 'Location' }
117
+
118
+ @headers['Location'] = response_location_header['value'] unless response_location_header.nil?
119
+ end
120
+
121
+ # Setup the Status header and body in the response.
122
+ def setup_status_and_body
123
+ @status, @body = @api_response['status'], [ @api_response['html'] ] # Note that the body in the Rack response must be an interable containing Strings, we it's wrapped within an Array.
124
+ end
125
+
126
+ # Setup Location and Status headers, as well as teh body, if we got a response from SnapSearch.
127
+ def setup_attributes_from_api_response
128
+ setup_location_header
129
+ # TODO: Should setup_content_length_header?
130
+ setup_status_and_body
131
+ end
132
+
133
+ def setup_attributes_from_app(environment)
134
+ @status, @headers, @body = @app.call(environment)
135
+ end
136
+
137
+ def rack_response
138
+ [ @status, @headers, @body ]
139
+ end
140
+
141
+ end
142
+
143
+ end
@@ -0,0 +1,85 @@
1
+ module Rack
2
+ class SnapSearch
3
+
4
+ # The configuration class for the Rack middleware.
5
+ # Holds the attributes to initialize the Client, Interceptor, and Detector with.
6
+ class Config
7
+
8
+ ATTRIBUTES = [
9
+ :email, :key, :api_url, :ca_cert_file, :x_forwarded_proto, :parameters, # Client options
10
+ :matched_routes, :ignored_routes, :robots_json, :extensions_json, :check_static_files # Detector options
11
+ ]
12
+
13
+ attr_accessor *ATTRIBUTES # Setup reader & writer instance methods for each attribute
14
+
15
+ # Create a new instance.
16
+ #
17
+ # @param [Hash] options The options to initialize this instance with.
18
+ # @option options [String] :email The email to authenticate with.
19
+ # @option options [String] :key The key to authenticate with.
20
+ # @option options [String] :api_url The API URL to send requests to.
21
+ # @option options [String] :ca_cert_file The CA Cert file to use when sending HTTPS requests to the API.
22
+ # @option options [String] :x_forwarded_proto Check X-Forwarded-Proto because Heroku SSL Support terminates at the load balancer
23
+ # @option options [String] :parameters Extra parameters to send to the API.
24
+ # @option options [String] :matched_routes Whitelisted routes. Should be an Array of Regexp instances.
25
+ # @option options [String] :ignored_routes Blacklisted routes. Should be an Array of Regexp instances.
26
+ # @option options [String] :robots_json A path of the JSON file containing the user agent whitelist & blacklist.
27
+ # @option options [String] :extensions_json A path to the JSON file containing a single Hash with the keys `ignore` and `match`. These keys contain Arrays of Strings (user agents)
28
+ # @option options [String] :check_static_files Set to `true` to ignore direct requests to files.
29
+ # @option options [Proc, #call] :on_exception The block to run when an exception within SnapSearch occurs.
30
+ # @option options [Proc, #call] :before_intercept A block to run before the interception of a bot.
31
+ # @option options [Proc, #call] :after_intercept A block to run after the interception of a bot.
32
+ # @option options [Proc, #call] :response_callback A block to manipulate the response from the SnapSearch API.
33
+ def initialize(options={})
34
+ raise TypeError, 'options must be a Hash or respond to #to_h' unless options.is_a?(Hash) || options.respond_to?(:to_h) || options.respond_to?(:to_hash)
35
+ options = options.to_h rescue options.to_hash
36
+
37
+ ATTRIBUTES.each do |attribute|
38
+ send( "#{attribute}=", options[attribute] ) if options.has_key?(attribute)
39
+ end
40
+ end
41
+
42
+ # Getter/Setter for the `on_exception` attribute.
43
+ #
44
+ # @yield If given, the Proc or callable to set the attribute as.
45
+ # @return [Proc] The value of the attribute.
46
+ def on_exception(&block)
47
+ @on_exception = block if block_given?
48
+
49
+ @on_exception
50
+ end
51
+
52
+ # Getter/Setter for the `before_intercept` attribute on the Interceptor.
53
+ #
54
+ # @yield If given, the Proc or callable to set the attribute as.
55
+ # @return [Proc] The value of the attribute.
56
+ def before_intercept(&block)
57
+ @before_intercept = block if block_given?
58
+
59
+ @before_intercept
60
+ end
61
+
62
+ # Getter/Setter for the `after_intercept` attribute on the Interceptor.
63
+ #
64
+ # @yield If given, the Proc or callable to set the attribute as.
65
+ # @return [Proc] The value of the attribute.
66
+ def after_intercept(&block)
67
+ @after_intercept = block if block_given?
68
+
69
+ @after_intercept
70
+ end
71
+
72
+ # Getter/Setter for the `response_callback` attribute on the Interceptor.
73
+ #
74
+ # @yield If given, the Proc or callable to set the attribute as.
75
+ # @return [Proc] The value of the attribute.
76
+ def response_callback(&block)
77
+ @response_callback = block if block_given?
78
+
79
+ @response_callback
80
+ end
81
+
82
+ end
83
+
84
+ end
85
+ end
@@ -0,0 +1,14 @@
1
+ require 'pathname'
2
+
3
+ module SnapSearch
4
+
5
+ def self.root
6
+ @root ||= Pathname.new(__FILE__).join('..', '..').expand_path
7
+ end
8
+
9
+ end
10
+
11
+ require 'snap_search/client'
12
+ require 'snap_search/detector'
13
+ require 'snap_search/interceptor'
14
+ require 'rack/snap_search'
@@ -0,0 +1,147 @@
1
+ require 'json'
2
+ require 'httpi'
3
+ require 'snap_search/connection_exception'
4
+ require 'snap_search/validation_exception'
5
+
6
+ module SnapSearch
7
+
8
+ # The Client sends an authenticated HTTP request to the SnapChat API and returns the `content`
9
+ # field from the JSON response body.
10
+ class Client
11
+
12
+ attr_reader :email, :key, :parameters, :api_url, :ca_cert_file
13
+
14
+ # Create a new Client instance.
15
+ #
16
+ # @param [Hash, #to_h] options The options to create the Client with.
17
+ # @option options [String, #to_s] :email The email to authenticate with.
18
+ # @option options [String, #to_s] :key The secret authentication key.
19
+ # @option options [Hash, #to_h] :parameters ({}) The parameters to send with the HTTP request.
20
+ # @option options [String, #to_s] :api_url (https://snapsearch.io/api/v1/robot) The URL to send the HTTP request to.
21
+ # @option options [String, #to_s] :ca_cert_file (ROOT/resources/cacert.pem) The CA cert file to use with request.
22
+ def initialize(options={})
23
+ initialize_attributes(options)
24
+ end
25
+
26
+ # Validate and set the value of the `email` attribute.
27
+ #
28
+ # @param [String, #to_s] value The value to set the attribute to.
29
+ # @return [String] The new value of the attribute.
30
+ def email=(value)
31
+ raise TypeError, 'email must be a String or respond to #to_s' unless value.is_a?(String) || respond_to?(:to_s)
32
+
33
+ value = value.to_s
34
+ raise ArgumentError, 'email must be an email address' unless value.include?(?@)
35
+
36
+ @email = value
37
+ end
38
+
39
+ # Validate and set the value of the `key` attribute.
40
+ #
41
+ # @param [String, #to_s] value The value to set the attribute to.
42
+ # @return [String] The new value of the attribute.
43
+ def key=(value)
44
+ @key = value.nil? ? nil : value.to_s
45
+ end
46
+
47
+ # Validate and set the value of the `parameters` attribute.
48
+ #
49
+ # @param [Hash, #to_h] value The value to set the attribute to.
50
+ # @return [Hash] The new value of the attribute.
51
+ def parameters=(value)
52
+ raise TypeError, 'parameters must be a Hash or respond to #to_h or #to_hash' unless value.is_a?(Hash) || value.respond_to?(:to_h) || value.respond_to?(:to_hash)
53
+ value = value.to_h rescue value.to_hash
54
+
55
+ @parameters = value.to_h
56
+ end
57
+
58
+ # Validate and set the value of the `api_url` attribute.
59
+ #
60
+ # @param [String, #to_s] value The value to set the attribute to.
61
+ # @return [String] The new value of the attribute.
62
+ def api_url=(value)
63
+ raise TypeError, 'api_url must be a String or respond_to #to_s' unless value.is_a?(String) || value.respond_to?(:to_s)
64
+
65
+ @api_url = value.to_s
66
+ end
67
+
68
+ # Validate and set the value of the `ca_cert_file` attribute.
69
+ #
70
+ # @param [String, #to_s] value The value to set the attribute to.
71
+ # @return [String] The new value of the attribute.
72
+ def ca_cert_file=(value)
73
+ raise TypeError, 'ca_cert_file must be a String or respond_to #to_s' unless value.is_a?(String) || value.respond_to?(:to_s)
74
+
75
+ @ca_cert_file = value.to_s
76
+ end
77
+
78
+ # Send an authenticated HTTP request to the `api_url` and return the `content` field from the JSON response body.
79
+ #
80
+ # @param [String, #to_s] url The url to send in the parameters to the `api_url`.
81
+ # @return [String] The `content` field from the JSON response body.
82
+ def request(url)
83
+ raise TypeError, 'url must be a String or respond_to #to_s' unless url.is_a?(String) || url.respond_to?(:to_s)
84
+ @parameters['url'] = url.to_s # The URL must contain the entire URL with the _escaped_fragment_ parsed out
85
+
86
+ content_from_response(send_request)
87
+ end
88
+
89
+ protected
90
+
91
+ # Initialize this instance based on options passed.
92
+ #
93
+ # @param [Hash, #to_h] options The options to create the Client with.
94
+ # @option options [String, #to_s] :email The email to authenticate with.
95
+ # @option options [String, #to_s] :key The secret authentication key.
96
+ # @option options [Hash, #to_h] :parameters ({}) The parameters to send with the HTTP request.
97
+ # @option options [String, #to_s] :api_url (https://snapsearch.io/api/v1/robot) The URL to send the HTTP request to.
98
+ # @option options [String, #to_s] :ca_cert_file (ROOT/resources/cacert.pem) The CA cert file to use with request.
99
+ def initialize_attributes(options)
100
+ raise TypeError, 'options must be a Hash or respond to #to_h' unless options.is_a?(Hash) || options.respond_to?(:to_h) || options.respond_to?(:to_hash)
101
+ options = options.to_h rescue options.to_hash
102
+
103
+ options = {
104
+ parameters: {},
105
+ api_url: 'https://snapsearch.io/api/v1/robot',
106
+ ca_cert_file: SnapSearch.root.join('resources', 'cacert.pem')
107
+ }.merge(options.to_h)
108
+
109
+ self.email, self.key, self.parameters, self.api_url, self.ca_cert_file = options.values_at(:email, :key, :parameters, :api_url, :ca_cert_file)
110
+ end
111
+
112
+ # Send an authenticated HTTP POST request encoded in JSON to the API URL
113
+ # using the HTTP client adapter of the developer's choice.
114
+ #
115
+ # @return [HTTPI::Response] The HTTP response object.
116
+ def send_request
117
+ request = HTTPI::Request.new
118
+ request.url = api_url
119
+ request.auth.basic(email, key)
120
+ request.auth.ssl.ca_cert_file = ca_cert_file
121
+ request.auth.ssl.verify_mode = :peer
122
+ request.open_timeout = 30
123
+ request.headers['Content-Type'] = 'application/json'
124
+ request.headers['Accept-Encoding'] = 'gzip, deflate, identity'
125
+ request.body = @parameters.to_json
126
+
127
+ HTTPI.post(request)
128
+ rescue
129
+ raise ConnectionException
130
+ end
131
+
132
+ # Retrieve the `content` or raise an error based on the `code` field in the JSON response.
133
+ #
134
+ # @return [HTTPI::Response] The HTTP response object.
135
+ def content_from_response(response)
136
+ body = JSON.parse(response.body)
137
+
138
+ case body['code']
139
+ when 'success' then body['content']
140
+ when 'validation_error' then raise( ValidationException, body['content'] )
141
+ else; false # System error on SnapSearch; Nothing we can do # TODO: Raise exception?
142
+ end
143
+ end
144
+
145
+ end
146
+
147
+ end