critsend_events 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.
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.rvmrc ADDED
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env bash
2
+
3
+ ruby_string="ruby-1.9.3"
4
+ gemset_name="critsend_events"
5
+
6
+ if rvm list strings | grep -q "${ruby_string}" ; then
7
+
8
+ # Load or create the specified environment
9
+ if [[ -d "${rvm_path:-$HOME/.rvm}/environments" \
10
+ && -s "${rvm_path:-$HOME/.rvm}/environments/${ruby_string}@${gemset_name}" ]] ; then
11
+ \. "${rvm_path:-$HOME/.rvm}/environments/${ruby_string}@${gemset_name}"
12
+ else
13
+ rvm --create "${ruby_string}@${gemset_name}"
14
+ fi
15
+
16
+ else
17
+
18
+ # Notify the user to install the desired interpreter before proceeding.
19
+ echo "${ruby_string} was not found, please run 'rvm install ${ruby_string}' and then cd back into the project directory."
20
+
21
+ fi
data/CHANGELOG.md ADDED
@@ -0,0 +1,3 @@
1
+ ## v1.0.0
2
+
3
+ * Initial release
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in critsend_events.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Alexander Glushkov
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,60 @@
1
+ # CritsendEvents
2
+
3
+ Accepts Critsend events with Ruby on Rails.
4
+ I hope that it will work with any Rack-based application, but I didn't verify that.
5
+
6
+ Check the [Critsend Event API](http://www.critsend.com/event-api/) for reference.
7
+
8
+ ## Installation
9
+
10
+ Add this line to your application's Gemfile:
11
+
12
+ gem 'critsend_events'
13
+
14
+ And then execute:
15
+
16
+ $ bundle
17
+
18
+ Or install it yourself as:
19
+
20
+ $ gem install critsend_events
21
+
22
+ ## Usage
23
+
24
+ - install it
25
+ - add middleware
26
+ - write an event receiver
27
+ - add config to initializers with Critsend authentication key
28
+
29
+ ```ruby
30
+ # application.rb
31
+
32
+ config.middleware.use "CritsendEvents::WebhookReceiver", "CritsendEventsReceiver"
33
+
34
+
35
+
36
+ # app/models/critsend_events_receiver.rb
37
+
38
+ class CritsendEventsReceiver
39
+ def handle(events)
40
+ Rails.logger.info "Events from Critsend: #{events.inspect}"
41
+ end
42
+ end
43
+
44
+
45
+
46
+ # config/initializers/critsend_config.rb
47
+
48
+ CritsendEvents::Config.setup do |config|
49
+ config.authentication_key = ENV["CRITSEND_WEBHOOKS_AUTH_KEY"]
50
+ # config.mount_point = "/critsend/receiver" // optional, default
51
+ end
52
+ ```
53
+
54
+ ## Contributing
55
+
56
+ 1. Fork it
57
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
58
+ 3. Commit your changes (`git commit -am 'Added some feature'`)
59
+ 4. Push to the branch (`git push origin my-new-feature`)
60
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
@@ -0,0 +1,21 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/critsend_events/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["Alexander Glushkov"]
6
+ gem.email = ["cutalion@gmail.com"]
7
+ gem.description = %q{}
8
+ gem.summary = %q{Accepts Critsend events with Ruby on Rails}
9
+ gem.homepage = ""
10
+
11
+ gem.files = `git ls-files`.split($\)
12
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
13
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
14
+ gem.name = "critsend_events"
15
+ gem.require_paths = ["lib"]
16
+ gem.version = CritsendEvents::VERSION
17
+ gem.add_dependency 'json'
18
+
19
+ gem.add_development_dependency 'rspec', '~> 2.0'
20
+ gem.add_development_dependency 'rake'
21
+ end
@@ -0,0 +1,8 @@
1
+ require 'openssl'
2
+ require 'json'
3
+
4
+ require "critsend_events/version"
5
+ require "critsend_events/config"
6
+ require "critsend_events/signature_generator"
7
+ require "critsend_events/webhook_processor"
8
+ require "critsend_events/webhook_receiver"
@@ -0,0 +1,17 @@
1
+ module CritsendEvents
2
+ class Config
3
+ class << self
4
+ def mount_point; @@mount_point; end
5
+ def mount_point=(val); @@mount_point = val; end
6
+ @@mount_point = "/critsend/receiver"
7
+
8
+ def authentication_key; @@authentication_key; end
9
+ def authentication_key=(val); @@authentication_key = val; end
10
+ @@authentication_key = ""
11
+
12
+ def setup
13
+ yield self
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,8 @@
1
+ module CritsendEvents
2
+ class SignatureGenerator
3
+ def self.generate(secret_key, message)
4
+ digest = OpenSSL::Digest::Digest.new('sha256')
5
+ OpenSSL::HMAC.hexdigest(digest, secret_key, message)
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,3 @@
1
+ module CritsendEvents
2
+ VERSION = "1.0.0"
3
+ end
@@ -0,0 +1,24 @@
1
+ module CritsendEvents
2
+ class WebhookProcessor
3
+ attr_reader :handler
4
+ def initialize(handler)
5
+ handler = constantize(handler).new if handler.is_a?(String)
6
+ @handler = handler
7
+ end
8
+
9
+ def process(data)
10
+ events = JSON.parse(data)
11
+ @handler.handle(events) if @handler.respond_to?(:handle)
12
+ end
13
+
14
+ private
15
+
16
+ def constantize(handler)
17
+ if handler.respond_to?(:constantize)
18
+ handler.constantize
19
+ else
20
+ Module.const_get(handler)
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,57 @@
1
+ require File.expand_path(__FILE__, 'signature_generator')
2
+ require File.expand_path(__FILE__, 'config')
3
+
4
+ module CritsendEvents
5
+ class WebhookReceiver
6
+ attr_reader :processor
7
+
8
+ def initialize(app, handler)
9
+ @app = app
10
+ @processor = WebhookProcessor.new(handler)
11
+ end
12
+
13
+ def call(env)
14
+ if events_posted?(env)
15
+ receive_events(env)
16
+ else
17
+ @app.call(env)
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ def events_posted?(env)
24
+ return false if env["REQUEST_METHOD"].to_s != "POST"
25
+ return env["PATH_INFO"].to_s == Config.mount_point
26
+ end
27
+
28
+ def receive_events(env)
29
+ if request_signed?(env)
30
+ process(env)
31
+ [200, {"Content-Type" => "application/json"}, StringIO.new("")]
32
+ else
33
+ [403, {"Content-Type" => "application/json"}, StringIO.new('{"error": "Invalid payload according to our webhooks key"}')]
34
+ end
35
+ end
36
+
37
+ def process(env)
38
+ env['rack.input'].rewind
39
+ data = env['rack.input'].read
40
+ processor.process data
41
+ end
42
+
43
+ def request_signed?(env)
44
+ critsend_signature = env["HTTP_X_CRITSEND_WEBHOOKS_SIGNATURE"]
45
+ data = env['rack.input'].read
46
+ secret_key = Config.authentication_key
47
+ our_signature = SignatureGenerator.generate(secret_key, data)
48
+
49
+ valid_signature?(critsend_signature, our_signature)
50
+ end
51
+
52
+ def valid_signature?(their_signature, our_signature)
53
+ return false if their_signature.to_s.empty?
54
+ our_signature === their_signature
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,24 @@
1
+ require 'spec_helper'
2
+
3
+ describe CritsendEvents::WebhookProcessor do
4
+ class TestHandler
5
+ def handle(*args); end
6
+ end
7
+
8
+ it "should initialize handler if handler is a string" do
9
+ processor = CritsendEvents::WebhookProcessor.new("TestHandler")
10
+ processor.handler.should be_a TestHandler
11
+ end
12
+
13
+ describe ".process" do
14
+ let(:raw_data) { '[{"category": "soft_bounce"}]' }
15
+ let(:parsed_data) { JSON.parse(raw_data) }
16
+ let(:handler) { TestHandler.new }
17
+
18
+ it "should convert raw data to json and pass it to registered handler" do
19
+ processor = CritsendEvents::WebhookProcessor.new(handler)
20
+ handler.should_receive(:handle).with(parsed_data)
21
+ processor.process(raw_data)
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,78 @@
1
+ require 'spec_helper'
2
+
3
+ describe CritsendEvents::WebhookReceiver do
4
+ let(:app) { stub }
5
+ let(:handler) { stub }
6
+ let(:middleware) { CritsendEvents::WebhookReceiver.new(app, handler) }
7
+ let(:env) { Hash.new }
8
+ let(:events_json) { "[]" }
9
+ before { CritsendEvents::Config.stub authentication_key: "key" }
10
+
11
+ context "if events posted" do
12
+ let(:env) { {"REQUEST_METHOD" => "POST", "PATH_INFO" => CritsendEvents::Config.mount_point} }
13
+ describe "receive events" do
14
+ before { env["rack.input"] = StringIO.new(events_json) }
15
+
16
+ context "if request not signed" do
17
+ it "should response with 403 Forbidden" do
18
+ response.status.should == 403
19
+ response.body.should == '{"error": "Invalid payload according to our webhooks key"}'
20
+ response.headers["Content-Type"].should == "application/json"
21
+ end
22
+ end
23
+
24
+ context "if request signed" do
25
+ before { env["HTTP_X_CRITSEND_WEBHOOKS_SIGNATURE"] = CritsendEvents::SignatureGenerator.generate(CritsendEvents::Config.authentication_key, events_json) }
26
+
27
+ it "should response with 200 OK" do
28
+ response.body.should == ''
29
+ response.status.should == 200
30
+ response.headers["Content-Type"].should == "application/json"
31
+ end
32
+
33
+ it "should call registered handler" do
34
+ middleware.processor.should_receive(:process).with(events_json)
35
+ middleware.call(env)
36
+ end
37
+ end
38
+
39
+ context "if request has incorrect signature" do
40
+ before { env["HTTP_X_CRITSEND_WEBHOOKS_SIGNATURE"] = "wrong-signature" }
41
+
42
+ it "should response with 403 Forbidden" do
43
+ response.status.should == 403
44
+ response.body.should == '{"error": "Invalid payload according to our webhooks key"}'
45
+ response.headers["Content-Type"].should == "application/json"
46
+ end
47
+ end
48
+ end
49
+ end
50
+
51
+ context "if events not posted" do
52
+ context "REQUEST_METHOD not POST" do
53
+ let(:env) { {"REQUEST_METHOD" => "GET", "PATH_INFO" => CritsendEvents::Config.mount_point} }
54
+ it "should pass the request to next middleware" do
55
+ app.should_receive(:call).with(env)
56
+ middleware.call(env)
57
+ end
58
+ end
59
+ context "PATH_INFO does not equal to mount point" do
60
+ let(:env) { {"REQUEST_METHOD" => "POST", "PATH_INFO" => "/"} }
61
+ it "should pass the request to next middleware" do
62
+ app.should_receive(:call).with(env)
63
+ middleware.call(env)
64
+ end
65
+ end
66
+ end
67
+
68
+ class Response < Struct.new(:status, :headers, :body)
69
+ def body
70
+ self[:body].read
71
+ end
72
+ end
73
+
74
+ def response
75
+ env["rack.input"].rewind
76
+ Response.new(*middleware.call(env))
77
+ end
78
+ end
@@ -0,0 +1 @@
1
+ require 'critsend_events'
metadata ADDED
@@ -0,0 +1,119 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: critsend_events
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Alexander Glushkov
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-08-30 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: json
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
30
+ - !ruby/object:Gem::Dependency
31
+ name: rspec
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ~>
36
+ - !ruby/object:Gem::Version
37
+ version: '2.0'
38
+ type: :development
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ~>
44
+ - !ruby/object:Gem::Version
45
+ version: '2.0'
46
+ - !ruby/object:Gem::Dependency
47
+ name: rake
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ description: ''
63
+ email:
64
+ - cutalion@gmail.com
65
+ executables: []
66
+ extensions: []
67
+ extra_rdoc_files: []
68
+ files:
69
+ - .gitignore
70
+ - .rvmrc
71
+ - CHANGELOG.md
72
+ - Gemfile
73
+ - LICENSE
74
+ - README.md
75
+ - Rakefile
76
+ - critsend_events.gemspec
77
+ - lib/critsend_events.rb
78
+ - lib/critsend_events/config.rb
79
+ - lib/critsend_events/signature_generator.rb
80
+ - lib/critsend_events/version.rb
81
+ - lib/critsend_events/webhook_processor.rb
82
+ - lib/critsend_events/webhook_receiver.rb
83
+ - spec/lib/critsend_events/webhook_processor_spec.rb
84
+ - spec/lib/critsend_events/webhook_receiver_spec.rb
85
+ - spec/spec_helper.rb
86
+ homepage: ''
87
+ licenses: []
88
+ post_install_message:
89
+ rdoc_options: []
90
+ require_paths:
91
+ - lib
92
+ required_ruby_version: !ruby/object:Gem::Requirement
93
+ none: false
94
+ requirements:
95
+ - - ! '>='
96
+ - !ruby/object:Gem::Version
97
+ version: '0'
98
+ segments:
99
+ - 0
100
+ hash: -4088613327361974571
101
+ required_rubygems_version: !ruby/object:Gem::Requirement
102
+ none: false
103
+ requirements:
104
+ - - ! '>='
105
+ - !ruby/object:Gem::Version
106
+ version: '0'
107
+ segments:
108
+ - 0
109
+ hash: -4088613327361974571
110
+ requirements: []
111
+ rubyforge_project:
112
+ rubygems_version: 1.8.24
113
+ signing_key:
114
+ specification_version: 3
115
+ summary: Accepts Critsend events with Ruby on Rails
116
+ test_files:
117
+ - spec/lib/critsend_events/webhook_processor_spec.rb
118
+ - spec/lib/critsend_events/webhook_receiver_spec.rb
119
+ - spec/spec_helper.rb