webhook_system 0.0.1

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 05503cb6981ef8a26c904b045fa7ad24b420f6f2
4
+ data.tar.gz: c870e8c64ffcc3dd09785b4e856e50ee385d1f3d
5
+ SHA512:
6
+ metadata.gz: 7f9fa83a0a0cdc22efac7c092807c86fc45e6df6bba6fa61d7b632dc33e146c90d58f7de1860abf15b9c92d0fd0b2683892b99b90b6c3fa690c750fa041770f2
7
+ data.tar.gz: 309d84dd3b30e2ebe343475ce7ac02fd316b617a86f0e2ba78eff1d2e8a15704b825a7ebf272089b8bb197732a4bd95bea148ec9a9f74300a7c50bbb04203d5a
data/.codeclimate.yml ADDED
@@ -0,0 +1,9 @@
1
+ engines:
2
+ rubocop:
3
+ enabled: true
4
+ ratings:
5
+ paths:
6
+ - "lib/**"
7
+ - Gemfile.lock
8
+ exclude_paths:
9
+ - "spec/*"
data/.gitignore ADDED
@@ -0,0 +1,5 @@
1
+ Gemfile.lock
2
+ doc/
3
+ pkg/
4
+ *.db
5
+ tmp/
data/.hound.yml ADDED
@@ -0,0 +1,2 @@
1
+ ruby:
2
+ config_file: .rubocop.yml
data/.reek ADDED
@@ -0,0 +1,10 @@
1
+ DuplicateMethodCall:
2
+ max_calls: 2
3
+ TooManyStatements:
4
+ max_statements: 8
5
+ NestedIterators:
6
+ max_allowed_nesting: 3
7
+ DataClump:
8
+ max_copies: 4
9
+ LongParameterList:
10
+ max_params: 8
@@ -0,0 +1,243 @@
1
+ AllCops:
2
+ Exclude:
3
+ - "vendor/**/*"
4
+ - "db/schema.rb"
5
+ UseCache: false
6
+ Style/CollectionMethods:
7
+ Description: Preferred collection methods.
8
+ StyleGuide: https://github.com/bbatsov/ruby-style-guide#map-find-select-reduce-size
9
+ Enabled: true
10
+ PreferredMethods:
11
+ collect: map
12
+ collect!: map!
13
+ find: detect
14
+ find_all: select
15
+ reduce: inject
16
+ Style/DotPosition:
17
+ Description: Checks the position of the dot in multi-line method calls.
18
+ StyleGuide: https://github.com/bbatsov/ruby-style-guide#consistent-multi-line-chains
19
+ Enabled: true
20
+ EnforcedStyle: trailing
21
+ SupportedStyles:
22
+ - leading
23
+ - trailing
24
+ Style/FileName:
25
+ Description: Use snake_case for source file names.
26
+ StyleGuide: https://github.com/bbatsov/ruby-style-guide#snake-case-files
27
+ Enabled: false
28
+ Exclude: []
29
+ Style/GuardClause:
30
+ Description: Check for conditionals that can be replaced with guard clauses
31
+ StyleGuide: https://github.com/bbatsov/ruby-style-guide#no-nested-conditionals
32
+ Enabled: false
33
+ MinBodyLength: 1
34
+ Style/IfUnlessModifier:
35
+ Description: Favor modifier if/unless usage when you have a single-line body.
36
+ StyleGuide: https://github.com/bbatsov/ruby-style-guide#if-as-a-modifier
37
+ Enabled: false
38
+ MaxLineLength: 80
39
+ Style/OptionHash:
40
+ Description: Don't use option hashes when you can use keyword arguments.
41
+ Enabled: false
42
+ Style/PercentLiteralDelimiters:
43
+ Description: Use `%`-literal delimiters consistently
44
+ StyleGuide: https://github.com/bbatsov/ruby-style-guide#percent-literal-braces
45
+ Enabled: false
46
+ PreferredDelimiters:
47
+ "%": "()"
48
+ "%i": "()"
49
+ "%q": "()"
50
+ "%Q": "()"
51
+ "%r": "{}"
52
+ "%s": "()"
53
+ "%w": "()"
54
+ "%W": "()"
55
+ "%x": "()"
56
+ Style/PredicateName:
57
+ Description: Check the names of predicate methods.
58
+ StyleGuide: https://github.com/bbatsov/ruby-style-guide#bool-methods-qmark
59
+ Enabled: true
60
+ NamePrefix:
61
+ - is_
62
+ - has_
63
+ - have_
64
+ NamePrefixBlacklist:
65
+ - is_
66
+ Exclude:
67
+ - spec/**/*
68
+ Style/RaiseArgs:
69
+ Description: Checks the arguments passed to raise/fail.
70
+ StyleGuide: https://github.com/bbatsov/ruby-style-guide#exception-class-messages
71
+ Enabled: false
72
+ EnforcedStyle: exploded
73
+ SupportedStyles:
74
+ - compact
75
+ - exploded
76
+ Style/SignalException:
77
+ Description: Checks for proper usage of fail and raise.
78
+ StyleGuide: https://github.com/bbatsov/ruby-style-guide#fail-method
79
+ Enabled: false
80
+ EnforcedStyle: semantic
81
+ SupportedStyles:
82
+ - only_raise
83
+ - only_fail
84
+ - semantic
85
+ Style/SingleLineBlockParams:
86
+ Description: Enforces the names of some block params.
87
+ StyleGuide: https://github.com/bbatsov/ruby-style-guide#reduce-blocks
88
+ Enabled: false
89
+ Methods:
90
+ - reduce:
91
+ - a
92
+ - e
93
+ - inject:
94
+ - a
95
+ - e
96
+ Style/SingleLineMethods:
97
+ Description: Avoid single-line methods.
98
+ StyleGuide: https://github.com/bbatsov/ruby-style-guide#no-single-line-methods
99
+ Enabled: false
100
+ AllowIfMethodIsEmpty: true
101
+ Style/StringLiterals:
102
+ Description: Checks if uses of quotes match the configured preference.
103
+ StyleGuide: https://github.com/bbatsov/ruby-style-guide#consistent-string-literals
104
+ Enabled: true
105
+ EnforcedStyle: double_quotes
106
+ SupportedStyles:
107
+ - single_quotes
108
+ - double_quotes
109
+ Style/StringLiteralsInInterpolation:
110
+ Description: Checks if uses of quotes inside expressions in interpolated strings
111
+ match the configured preference.
112
+ Enabled: true
113
+ EnforcedStyle: single_quotes
114
+ SupportedStyles:
115
+ - single_quotes
116
+ - double_quotes
117
+ Style/TrailingCommaInArguments:
118
+ Description: 'Checks for trailing comma in argument lists.'
119
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-trailing-array-commas'
120
+ Enabled: false
121
+ EnforcedStyleForMultiline: no_comma
122
+ SupportedStyles:
123
+ - comma
124
+ - consistent_comma
125
+ - no_comma
126
+ Style/TrailingCommaInLiteral:
127
+ Description: 'Checks for trailing comma in array and hash literals.'
128
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-trailing-array-commas'
129
+ Enabled: false
130
+ EnforcedStyleForMultiline: no_comma
131
+ SupportedStyles:
132
+ - comma
133
+ - consistent_comma
134
+ - no_comma
135
+ Metrics/AbcSize:
136
+ Description: A calculated magnitude based on number of assignments, branches, and
137
+ conditions.
138
+ Enabled: false
139
+ Max: 15
140
+ Metrics/ClassLength:
141
+ Description: Avoid classes longer than 100 lines of code.
142
+ Enabled: false
143
+ CountComments: false
144
+ Max: 100
145
+ Metrics/ModuleLength:
146
+ CountComments: false
147
+ Max: 100
148
+ Description: Avoid modules longer than 100 lines of code.
149
+ Enabled: false
150
+ Metrics/CyclomaticComplexity:
151
+ Description: A complexity metric that is strongly correlated to the number of test
152
+ cases needed to validate a method.
153
+ Enabled: false
154
+ Max: 6
155
+ Metrics/MethodLength:
156
+ Description: Avoid methods longer than 10 lines of code.
157
+ StyleGuide: https://github.com/bbatsov/ruby-style-guide#short-methods
158
+ Enabled: false
159
+ CountComments: false
160
+ Max: 10
161
+ Metrics/ParameterLists:
162
+ Description: Avoid parameter lists longer than three or four parameters.
163
+ StyleGuide: https://github.com/bbatsov/ruby-style-guide#too-many-params
164
+ Enabled: false
165
+ Max: 5
166
+ CountKeywordArgs: true
167
+ Metrics/PerceivedComplexity:
168
+ Description: A complexity metric geared towards measuring complexity for a human
169
+ reader.
170
+ Enabled: false
171
+ Max: 7
172
+ Lint/AssignmentInCondition:
173
+ Description: Don't use assignment in conditions.
174
+ StyleGuide: https://github.com/bbatsov/ruby-style-guide#safe-assignment-in-condition
175
+ Enabled: false
176
+ AllowSafeAssignment: true
177
+ Style/InlineComment:
178
+ Description: Avoid inline comments.
179
+ Enabled: false
180
+ Style/AccessorMethodName:
181
+ Description: Check the naming of accessor methods for get_/set_.
182
+ Enabled: false
183
+ Style/Alias:
184
+ Description: Use alias_method instead of alias.
185
+ StyleGuide: https://github.com/bbatsov/ruby-style-guide#alias-method
186
+ Enabled: false
187
+ Style/Documentation:
188
+ Description: Document classes and non-namespace modules.
189
+ Enabled: false
190
+ Style/DoubleNegation:
191
+ Description: Checks for uses of double negation (!!).
192
+ StyleGuide: https://github.com/bbatsov/ruby-style-guide#no-bang-bang
193
+ Enabled: false
194
+ Style/EachWithObject:
195
+ Description: Prefer `each_with_object` over `inject` or `reduce`.
196
+ Enabled: false
197
+ Style/EmptyLiteral:
198
+ Description: Prefer literals to Array.new/Hash.new/String.new.
199
+ StyleGuide: https://github.com/bbatsov/ruby-style-guide#literal-array-hash
200
+ Enabled: false
201
+ Style/ModuleFunction:
202
+ Description: Checks for usage of `extend self` in modules.
203
+ StyleGuide: https://github.com/bbatsov/ruby-style-guide#module-function
204
+ Enabled: false
205
+ Style/OneLineConditional:
206
+ Description: Favor the ternary operator(?:) over if/then/else/end constructs.
207
+ StyleGuide: https://github.com/bbatsov/ruby-style-guide#ternary-operator
208
+ Enabled: false
209
+ Style/PerlBackrefs:
210
+ Description: Avoid Perl-style regex back references.
211
+ StyleGuide: https://github.com/bbatsov/ruby-style-guide#no-perl-regexp-last-matchers
212
+ Enabled: false
213
+ Style/Send:
214
+ Description: Prefer `Object#__send__` or `Object#public_send` to `send`, as `send`
215
+ may overlap with existing methods.
216
+ StyleGuide: https://github.com/bbatsov/ruby-style-guide#prefer-public-send
217
+ Enabled: false
218
+ Style/SpecialGlobalVars:
219
+ Description: Avoid Perl-style global variables.
220
+ StyleGuide: https://github.com/bbatsov/ruby-style-guide#no-cryptic-perlisms
221
+ Enabled: false
222
+ Style/VariableInterpolation:
223
+ Description: Don't interpolate global, instance and class variables directly in
224
+ strings.
225
+ StyleGuide: https://github.com/bbatsov/ruby-style-guide#curlies-interpolate
226
+ Enabled: false
227
+ Style/WhenThen:
228
+ Description: Use when x then ... for one-line cases.
229
+ StyleGuide: https://github.com/bbatsov/ruby-style-guide#one-line-cases
230
+ Enabled: false
231
+ Lint/EachWithObjectArgument:
232
+ Description: Check for immutable argument given to each_with_object.
233
+ Enabled: true
234
+ Lint/HandleExceptions:
235
+ Description: Don't suppress exception.
236
+ StyleGuide: https://github.com/bbatsov/ruby-style-guide#dont-hide-exceptions
237
+ Enabled: false
238
+ Lint/LiteralInCondition:
239
+ Description: Checks of literals used in conditions.
240
+ Enabled: false
241
+ Lint/LiteralInInterpolation:
242
+ Description: Checks for literals used in interpolation.
243
+ Enabled: false
data/.rubocop.yml ADDED
@@ -0,0 +1,57 @@
1
+ inherit_from:
2
+ - .rubocop.hound.yml
3
+
4
+ AllCops:
5
+ Exclude:
6
+ - '*.gemspec'
7
+ - 'Gemfile'
8
+
9
+ Style/Lambda:
10
+ Enabled: false
11
+
12
+ Style/EmptyLinesAroundClassBody:
13
+ Enabled: false
14
+
15
+ Style/EmptyLinesAroundModuleBody:
16
+ Enabled: false
17
+
18
+ Style/EmptyLinesAroundMethodBody:
19
+ Enabled: false
20
+
21
+ Style/ClassCheck:
22
+ Enabled: false
23
+ # we don't care about kind_of? vs is_a?
24
+
25
+ Style/StringLiterals:
26
+ Enabled: false
27
+
28
+ Style/FileName:
29
+ Enabled: false
30
+
31
+ Style/RedundantException:
32
+ Enabled: false
33
+
34
+ Style/SignalException:
35
+ Enabled: false
36
+
37
+ Style/BlockDelimiters:
38
+ Enabled: false
39
+
40
+ Style/CollectionMethods:
41
+ PreferredMethods:
42
+ detect: find
43
+
44
+ # Github's PR width is 120 characters
45
+ Metrics/LineLength:
46
+ Max: 120
47
+ AllowURI: true
48
+
49
+ # Align with the style guide, we don't prefer anything
50
+ Style/CollectionMethods:
51
+ Enabled: false
52
+
53
+ Metrics/AbcSize:
54
+ Description: A calculated magnitude based on number of assignments, branches, and
55
+ conditions.
56
+ Enabled: true
57
+ Max: 30
data/.travis.yml ADDED
@@ -0,0 +1,7 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.1.8
4
+ - 2.2.4
5
+ - 2.3.0
6
+
7
+ script: "bundle exec rspec"
data/CHANGELOG.md ADDED
File without changes
data/DEPLOYING.md ADDED
@@ -0,0 +1,25 @@
1
+ ## Setup
2
+
3
+ In order to deploy a new version of the gem into the wild ...
4
+
5
+ You will need to configure your github api token for the changelog.
6
+
7
+ Generate a new token for changelogs [here](https://github.com/settings/tokens/new).
8
+
9
+ add:
10
+
11
+ ```bash
12
+ export CHANGELOG_GITHUB_TOKEN=YOUR_CHANGELOG_API_TOKEN
13
+ ```
14
+
15
+ somewhere in your shell init. (ie .zshrc or simillar)
16
+
17
+ ## Deploying
18
+
19
+ 1. Update `lib/webhook_system/version.rb`
20
+ 2. Commit the changed files with the version number eg: `1.8.0`
21
+ 3. Push this to git
22
+ 4. Run `rake release`
23
+ 5. Run `rake changelog` (has to be ran after release since its based on github tagging)
24
+ 6. Commit the changed changelog named something like `changelog for 1.8.0`
25
+ 7. Push this to git
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,12 @@
1
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
2
+ documentation files (the "Software"), to deal in the Software without restriction, including without limitation
3
+ the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
4
+ and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
5
+
6
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of
7
+ the Software.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO
10
+ THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
11
+ OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
12
+ OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,155 @@
1
+ # webhook_system
2
+
3
+ * [Homepage](https://rubygems.org/gems/webhook_system)
4
+ * [Documentation](http://rubydoc.info/gems/webhook_system/frames)
5
+ * [Email](mailto:piotr.banasik at gmail.com)
6
+
7
+ ## Description
8
+
9
+ [![Build Status](https://travis-ci.org/payrollhero/webhook_engine.svg?branch=master)](https://travis-ci.org/payrollhero/webhook_engine)
10
+ [![Code Climate](https://codeclimate.com/github/payrollhero/webhook_system/badges/gpa.svg)](https://codeclimate.com/github/payrollhero/webhook_system)
11
+ [![Issue Count](https://codeclimate.com/github/payrollhero/webhook_system/badges/issue_count.svg)](https://codeclimate.com/github/payrollhero/webhook_system)
12
+ [![Dependency Status](https://gemnasium.com/payrollhero/webhook_system.svg)](https://gemnasium.com/payrollhero/webhook_system)
13
+
14
+ ## Overview
15
+
16
+ Few Main points ..
17
+
18
+ 1. The "server" holds on to a record of subscriptions
19
+ 2. Each subscription has a secret attached to it
20
+ 3. This secret is used to encrypt the entire payload of the webhook using AES-256
21
+ 4. The webhook is delivered to the recipient as a JSON payload with a base64 encoded data component
22
+ 5. The recipient is meant to use their copy of this secret to decode that payload, and then action it as needed.
23
+
24
+ ## Setup
25
+
26
+ The webhook integration code runs on two tables. You need to create a new migration that adds these
27
+ tables first:
28
+
29
+ ```ruby
30
+ create_table :webhook_subscriptions do |t|
31
+ t.string :url
32
+ t.boolean :active
33
+ t.text :secret
34
+
35
+ t.index :active
36
+ end
37
+
38
+ create_table :webhook_subscription_topics do |t|
39
+ t.string :name
40
+ t.belongs_to :subscription
41
+
42
+ t.index :subscription_id
43
+ t.index :name
44
+ end
45
+ ```
46
+
47
+ ## Building Events
48
+
49
+ Each event type should be a discrete class inheriting from `WebhookSystem::BaseEvent`.
50
+
51
+ Each class should define `event_name` and `payload_attributes` to set up the serializer properly.
52
+
53
+ ### Payload Attributes
54
+
55
+ It seems worth mentioning a bit more about these. This whole system in the event is an abstraction system around
56
+ cleanly serializing these objects. The attribute list can take two forms:
57
+
58
+ Either as a simple array:
59
+
60
+ ```ruby
61
+ def payload_attributes
62
+ [
63
+ :widget,
64
+ :name,
65
+ ]
66
+ end
67
+ ```
68
+
69
+ This version just maps 1:1 the attribute to a method on the event. This means that in this example it'd expect
70
+ `widget` and `name` to be public methods on the event.
71
+
72
+ The alternate form is to use a Hash.
73
+
74
+ eg:
75
+ ```ruby
76
+ def payload_attributes
77
+ {
78
+ widget: :get_widget,
79
+ name: :get_name,
80
+ }
81
+ end
82
+ ```
83
+
84
+ This version makes the keys the attributes in the JSON, and the values the method names to call to get the value.
85
+
86
+ ### Working with Events
87
+
88
+ The general idea is that Events are ActiveModel objects using the PhModel system, so the same APIs apply.
89
+
90
+ You'd build them with `.build`. Eg:
91
+
92
+ ```ruby
93
+ event_object = SomeEvent.build(name: 'John', age: 21)
94
+ ```
95
+
96
+ These attributes shouldn't necessarily be already the direct data hashes, let the Event do its own presentation.
97
+ Its perfectly OK to send these events with ActiveRecord parameters, and internally translate out of them to something
98
+ more suitable for the actual notification payload.
99
+
100
+ ## Dispatching Events
101
+
102
+ The general API for this is via:
103
+
104
+ ```ruby
105
+ WebhookSystem.dispatch(event_object)
106
+ ```
107
+
108
+ This is meant to be fairly fire and forget. Internally this will create an ActiveJob for each subscription
109
+ interested in the event.
110
+
111
+ ## Payload Encryption
112
+
113
+ The payload is encrypted using AES-256. Each subscription is meant to have the recipient's shared secret on it.
114
+ This secret is then used to encrypt the payload, so the other side needs that same secret again to open it.
115
+
116
+ The payload then will be a json post body, with the Base64 encoded payload inside it.
117
+
118
+ ## Payload Decryption
119
+
120
+ There is a utility function available to decode the entire POST body of the webhook that can be used by clients.
121
+
122
+ Example use would be:
123
+
124
+ ```ruby
125
+ payload = WebhookSystem::Encoder.decode(secret_string, request.body)
126
+ ```
127
+
128
+ You will need your webhook secret, and you get back a Hash of the event's data.
129
+ All events are guaranteed to have a key called `event` with the event's name, everything other than that is
130
+ custom to each individual event type.
131
+
132
+ Some more specifics around the format for the benefit of non-ruby implementations:
133
+
134
+ The payload looks like this:
135
+
136
+ ```json
137
+ {
138
+ "format": "base64+aes256",
139
+ "payload": "{Base64 String}",
140
+ "iv": "{Base64 String}"
141
+ }
142
+ ```
143
+
144
+ The format is just a flag incase there ever is multiple formats, currently this is the only one.
145
+
146
+ The encryption used is AES256-CBC
147
+
148
+ The AES key is a PBKDF2 (Password-Based Key Derivation Function 2) as part of PKCS#5 function on the secret using
149
+ SHA256 HMAC. The IV is used as the salt, 100_000 iterations, and a key length of 32 bytes (or 256 bits).
150
+
151
+ The IV is used both for initializing the cipher, and also for salting the password PBKDF2 function.
152
+
153
+ ## Copyright
154
+
155
+ Copyright (c) 2015 PayrollHero
data/Rakefile ADDED
@@ -0,0 +1,46 @@
1
+ require 'bundler/gem_tasks'
2
+
3
+ require 'rubygems/tasks'
4
+ Gem::Tasks.new(release: false)
5
+
6
+ require 'rspec/core/rake_task'
7
+ RSpec::Core::RakeTask.new
8
+
9
+ task :test do
10
+ sh "rspec"
11
+ sh "reek"
12
+ sh "rubocop"
13
+ end
14
+
15
+ task default: :spec
16
+
17
+ desc "copy in PayrollHero's current style config files"
18
+ task :styleguide do
19
+ require 'faraday'
20
+ require 'pry'
21
+ base = "https://raw.githubusercontent.com/payrollhero/styleguide/master/"
22
+ files = %w{
23
+ .rubocop.hound.yml
24
+ .rubocop.yml
25
+ .reek
26
+ .codeclimate.yml
27
+ }
28
+ files.each do |file|
29
+ puts "Fetching #{file} ..."
30
+ url = "#{base}#{file}"
31
+ rsp = Faraday.get(url)
32
+ unless rsp.status == 200
33
+ $stderr.puts "failing fetching: #{url}"
34
+ $stderr.puts " response: #{rsp.status}: #{rsp.body}"
35
+ exit 1
36
+ end
37
+ File.open(file, "w") do |fh|
38
+ fh.write(rsp.body)
39
+ end
40
+ end
41
+ end
42
+
43
+ desc "Updates the changelog"
44
+ task :changelog do
45
+ sh "github_changelog_generator payrollhero/ph_utility --simple-list"
46
+ end
@@ -0,0 +1,23 @@
1
+ require 'active_support/all'
2
+ require 'active_record'
3
+ require 'active_job'
4
+ require 'ph_model'
5
+
6
+ module WebhookSystem
7
+ extend ActiveSupport::Autoload
8
+
9
+ autoload :Subscription
10
+ autoload :Dispatcher
11
+ autoload :SubscriptionTopic
12
+ autoload :Job
13
+ autoload :Encoder
14
+ autoload :BaseEvent
15
+
16
+ # Error raised when there is an issue with decoding the payload
17
+ class DecodingError < RuntimeError
18
+ end
19
+
20
+ class << self
21
+ delegate :dispatch, to: :'WebhookSystem::Dispatcher'
22
+ end
23
+ end
@@ -0,0 +1,40 @@
1
+ module WebhookSystem
2
+
3
+ # This is the class meant to be used as the base class for any Events sent through the Webhook system
4
+ class BaseEvent
5
+ include PhModel
6
+
7
+ def event_name
8
+ mesg = "class #{self.class.name} must implement abstract method `#{self.class.name}#event_name()'."
9
+ raise RuntimeError.new(mesg).tap { |err| err.backtrace = caller }
10
+ end
11
+
12
+ def payload_attributes
13
+ mesg = "class #{self.class.name} must implement abstract method `#{self.class.name}#payload_attributes()'."
14
+ raise RuntimeError.new(mesg).tap { |err| err.backtrace = caller }
15
+ end
16
+
17
+ def as_json
18
+ result = { 'event' => event_name }
19
+ each_attribute do |attribute_name, attribute_method|
20
+ result[attribute_name.to_s] = public_send(attribute_method).as_json
21
+ end
22
+ result
23
+ end
24
+
25
+ private
26
+
27
+ def each_attribute
28
+ case payload_attributes
29
+ when Array
30
+ payload_attributes.each do |attribute_name|
31
+ yield(attribute_name, attribute_name)
32
+ end
33
+ when Hash
34
+ payload_attributes.each
35
+ else
36
+ raise ArgumetError, "don't know how to deal with payload_attributes: #{payload_attributes.inspect}"
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,16 @@
1
+ module WebhookSystem
2
+
3
+ # Main code that handles dispatching of events out to subscribers
4
+ class Dispatcher
5
+ class << self
6
+
7
+ # @param [WebhookSystem::BaseEvent] event The Event Object
8
+ def dispatch(event)
9
+ WebhookSystem::Subscription.interested_in_topic(event.event_name).each do |subscription|
10
+ WebhookSystem::Job.perform_later subscription, event.as_json
11
+ end
12
+ end
13
+
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,69 @@
1
+ module WebhookSystem
2
+
3
+ # Class in charge of encoding and decoding encrypted payload
4
+ module Encoder
5
+ class << self
6
+ # Given a secret string, encode the passed payload to json
7
+ # encrypt it, base64 encode that, and wrap it in its own json wrapper
8
+ #
9
+ # @param [String] secret_string some secret string
10
+ # @param [Object#to_json] payload Any object that responds to to_json
11
+ # @return [String] The encoded string payload (its a JSON string)
12
+ def encode(secret_string, payload)
13
+ cipher = OpenSSL::Cipher::AES256.new(:CBC)
14
+ cipher.encrypt
15
+ iv = cipher.random_iv
16
+ cipher.key = key_from_secret(iv, secret_string)
17
+ encoded = cipher.update(payload.to_json) + cipher.final
18
+ Payload.encode(encoded, iv)
19
+ end
20
+
21
+ # Given a secret string, and an encrypted payload, unwrap it, bas64 decode it
22
+ # decrypt it, and JSON decode it
23
+ #
24
+ # @param [String] secret_string some secret string
25
+ # @param [String] payload String as returned from #encode
26
+ # @return [Object] return the JSON decode of the encrypted payload
27
+ def decode(secret_string, payload)
28
+ encoded, iv = Payload.decode(payload)
29
+ cipher = OpenSSL::Cipher::AES256.new(:CBC)
30
+ cipher.decrypt
31
+ cipher.iv = iv
32
+ cipher.key = key_from_secret(iv, secret_string)
33
+ decoded = cipher.update(encoded) + cipher.final
34
+ JSON.load(decoded)
35
+ rescue OpenSSL::Cipher::CipherError
36
+ raise DecodingError, 'Decoding Failed, probably mismatched secret'
37
+ end
38
+
39
+ private
40
+
41
+ def key_from_secret(iv, secret_string)
42
+ OpenSSL::PKCS5.pbkdf2_hmac(secret_string, iv, 100_000, 256 / 8, 'SHA256')
43
+ end
44
+ end
45
+ end
46
+
47
+ # private class to just wrap the outer wrapping of the response format
48
+ # not exposed to the outside
49
+ # :nodoc:
50
+ module Payload
51
+ class << self
52
+ def encode(raw_encrypted_data, iv)
53
+ JSON.dump(
54
+ 'format' => 'base64+aes256',
55
+ 'payload' => Base64.encode64(raw_encrypted_data),
56
+ 'iv' => Base64.encode64(iv)
57
+ )
58
+ end
59
+
60
+ def decode(payload_string)
61
+ payload = JSON.load(payload_string)
62
+ unless payload['format'] == 'base64+aes256'
63
+ raise ArgumentError, 'only know how to handle base64+aes256 payloads'
64
+ end
65
+ [Base64.decode64(payload['payload']), Base64.decode64(payload['iv'])]
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,39 @@
1
+ module WebhookSystem
2
+
3
+ # This is the ActiveJob in charge of actually sending each event
4
+ class Job < ActiveJob::Base
5
+
6
+ def perform(subscription, event)
7
+ payload = Encoder.encode(subscription.secret, event)
8
+ client = HttpClient.new(subscription.url)
9
+ client.post(payload)
10
+ end
11
+
12
+ end
13
+
14
+ # Just a simple internal class to wrap around the http requests to the endpoints
15
+ class HttpClient
16
+ def initialize(endpoint)
17
+ @endpoint = endpoint
18
+ end
19
+
20
+ def post(payload)
21
+ client.post do |req|
22
+ req.headers['Content-Type'] = 'application/json; base64+aes256'
23
+ req.body = payload.to_s
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ attr_reader :endpoint, :client
30
+
31
+ def client
32
+ @client ||= Faraday.new(url: endpoint) do |faraday|
33
+ # faraday.request :url_encoded # form-encode POST params
34
+ faraday.response :logger # log requests to STDOUT
35
+ faraday.adapter Faraday.default_adapter
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,17 @@
1
+ module WebhookSystem
2
+ class Subscription < ActiveRecord::Base
3
+ self.table_name = 'webhook_subscriptions'
4
+
5
+ validates :url, presence: true
6
+ validates :secret, presence: true
7
+
8
+ has_many :topics, class_name: 'WebhookSystem::SubscriptionTopic', dependent: :destroy
9
+
10
+ scope :active, -> { where(active: true) }
11
+ scope :for_topic, -> (topic) {
12
+ joins(:topics).where(WebhookSystem::SubscriptionTopic.table_name => { name: topic })
13
+ }
14
+
15
+ scope :interested_in_topic, -> (topic) { active.for_topic(topic) }
16
+ end
17
+ end
@@ -0,0 +1,9 @@
1
+ module WebhookSystem
2
+ class SubscriptionTopic < ActiveRecord::Base
3
+ self.table_name = 'webhook_subscription_topics'
4
+
5
+ validates :name, presence: true
6
+
7
+ belongs_to :subscription, class_name: 'WebhookSystem::Subscription'
8
+ end
9
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+ module WebhookSystem
3
+ VERSION = '0.0.1'.freeze
4
+ end
@@ -0,0 +1,40 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ require File.expand_path('../lib/webhook_system/version', __FILE__)
4
+
5
+ Gem::Specification.new do |gem|
6
+ gem.name = 'webhook_system'
7
+ gem.version = WebhookSystem::VERSION
8
+ gem.authors = ['Piotr Banasik']
9
+ gem.email = 'piotr@payrollhero.com'
10
+
11
+ gem.summary = 'Webhook system'
12
+ gem.description = 'A pluggable webhook subscription system'
13
+ gem.homepage = 'https://github.com/payrollhero/webhook_system'
14
+ gem.license = 'MIT'
15
+
16
+ gem.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
17
+ gem.bindir = 'exe'
18
+ gem.executables = gem.files.grep(%r{^bin/}).map { |f| File.basename(f) }
19
+ gem.require_paths = ['lib']
20
+
21
+ gem.add_runtime_dependency 'activesupport', '> 3.2'
22
+ gem.add_runtime_dependency 'activerecord', '> 3.2'
23
+ gem.add_runtime_dependency 'activejob'
24
+ gem.add_runtime_dependency 'faraday'
25
+ gem.add_runtime_dependency 'ph_model'
26
+
27
+ gem.add_development_dependency 'bundler', '~> 1.0'
28
+ gem.add_development_dependency 'rake'
29
+ gem.add_development_dependency 'rspec', '~> 3.0'
30
+ gem.add_development_dependency 'rubygems-tasks', '~> 0.2'
31
+ gem.add_development_dependency 'pry'
32
+ gem.add_development_dependency 'sqlite3'
33
+ gem.add_development_dependency 'github_changelog_generator', '~> 1.6'
34
+ gem.add_development_dependency 'factory_girl'
35
+ gem.add_development_dependency 'webmock'
36
+
37
+ # static analysis gems
38
+ gem.add_development_dependency 'rubocop', '~> 0.36.0'
39
+ gem.add_development_dependency 'reek', '~> 3.7'
40
+ end
metadata ADDED
@@ -0,0 +1,289 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: webhook_system
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Piotr Banasik
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2016-02-05 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activesupport
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">"
18
+ - !ruby/object:Gem::Version
19
+ version: '3.2'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">"
25
+ - !ruby/object:Gem::Version
26
+ version: '3.2'
27
+ - !ruby/object:Gem::Dependency
28
+ name: activerecord
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">"
32
+ - !ruby/object:Gem::Version
33
+ version: '3.2'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">"
39
+ - !ruby/object:Gem::Version
40
+ version: '3.2'
41
+ - !ruby/object:Gem::Dependency
42
+ name: activejob
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: faraday
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: ph_model
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: bundler
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '1.0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '1.0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rake
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: rspec
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: '3.0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: '3.0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: rubygems-tasks
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: '0.2'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - "~>"
137
+ - !ruby/object:Gem::Version
138
+ version: '0.2'
139
+ - !ruby/object:Gem::Dependency
140
+ name: pry
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - ">="
144
+ - !ruby/object:Gem::Version
145
+ version: '0'
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - ">="
151
+ - !ruby/object:Gem::Version
152
+ version: '0'
153
+ - !ruby/object:Gem::Dependency
154
+ name: sqlite3
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - ">="
158
+ - !ruby/object:Gem::Version
159
+ version: '0'
160
+ type: :development
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - ">="
165
+ - !ruby/object:Gem::Version
166
+ version: '0'
167
+ - !ruby/object:Gem::Dependency
168
+ name: github_changelog_generator
169
+ requirement: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - "~>"
172
+ - !ruby/object:Gem::Version
173
+ version: '1.6'
174
+ type: :development
175
+ prerelease: false
176
+ version_requirements: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - "~>"
179
+ - !ruby/object:Gem::Version
180
+ version: '1.6'
181
+ - !ruby/object:Gem::Dependency
182
+ name: factory_girl
183
+ requirement: !ruby/object:Gem::Requirement
184
+ requirements:
185
+ - - ">="
186
+ - !ruby/object:Gem::Version
187
+ version: '0'
188
+ type: :development
189
+ prerelease: false
190
+ version_requirements: !ruby/object:Gem::Requirement
191
+ requirements:
192
+ - - ">="
193
+ - !ruby/object:Gem::Version
194
+ version: '0'
195
+ - !ruby/object:Gem::Dependency
196
+ name: webmock
197
+ requirement: !ruby/object:Gem::Requirement
198
+ requirements:
199
+ - - ">="
200
+ - !ruby/object:Gem::Version
201
+ version: '0'
202
+ type: :development
203
+ prerelease: false
204
+ version_requirements: !ruby/object:Gem::Requirement
205
+ requirements:
206
+ - - ">="
207
+ - !ruby/object:Gem::Version
208
+ version: '0'
209
+ - !ruby/object:Gem::Dependency
210
+ name: rubocop
211
+ requirement: !ruby/object:Gem::Requirement
212
+ requirements:
213
+ - - "~>"
214
+ - !ruby/object:Gem::Version
215
+ version: 0.36.0
216
+ type: :development
217
+ prerelease: false
218
+ version_requirements: !ruby/object:Gem::Requirement
219
+ requirements:
220
+ - - "~>"
221
+ - !ruby/object:Gem::Version
222
+ version: 0.36.0
223
+ - !ruby/object:Gem::Dependency
224
+ name: reek
225
+ requirement: !ruby/object:Gem::Requirement
226
+ requirements:
227
+ - - "~>"
228
+ - !ruby/object:Gem::Version
229
+ version: '3.7'
230
+ type: :development
231
+ prerelease: false
232
+ version_requirements: !ruby/object:Gem::Requirement
233
+ requirements:
234
+ - - "~>"
235
+ - !ruby/object:Gem::Version
236
+ version: '3.7'
237
+ description: A pluggable webhook subscription system
238
+ email: piotr@payrollhero.com
239
+ executables: []
240
+ extensions: []
241
+ extra_rdoc_files: []
242
+ files:
243
+ - ".codeclimate.yml"
244
+ - ".gitignore"
245
+ - ".hound.yml"
246
+ - ".reek"
247
+ - ".rubocop.hound.yml"
248
+ - ".rubocop.yml"
249
+ - ".travis.yml"
250
+ - CHANGELOG.md
251
+ - DEPLOYING.md
252
+ - Gemfile
253
+ - LICENSE.txt
254
+ - README.md
255
+ - Rakefile
256
+ - lib/webhook_system.rb
257
+ - lib/webhook_system/base_event.rb
258
+ - lib/webhook_system/dispatcher.rb
259
+ - lib/webhook_system/encoder.rb
260
+ - lib/webhook_system/job.rb
261
+ - lib/webhook_system/subscription.rb
262
+ - lib/webhook_system/subscription_topic.rb
263
+ - lib/webhook_system/version.rb
264
+ - webhook_system.gemspec
265
+ homepage: https://github.com/payrollhero/webhook_system
266
+ licenses:
267
+ - MIT
268
+ metadata: {}
269
+ post_install_message:
270
+ rdoc_options: []
271
+ require_paths:
272
+ - lib
273
+ required_ruby_version: !ruby/object:Gem::Requirement
274
+ requirements:
275
+ - - ">="
276
+ - !ruby/object:Gem::Version
277
+ version: '0'
278
+ required_rubygems_version: !ruby/object:Gem::Requirement
279
+ requirements:
280
+ - - ">="
281
+ - !ruby/object:Gem::Version
282
+ version: '0'
283
+ requirements: []
284
+ rubyforge_project:
285
+ rubygems_version: 2.4.5.1
286
+ signing_key:
287
+ specification_version: 4
288
+ summary: Webhook system
289
+ test_files: []