snapsearch-client-ruby 0.1.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.
@@ -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