hooks-ruby 0.0.6 → 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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9025d70b78a67040f436318091d556090c7f17e9dffe094b6437bde394ef7010
4
- data.tar.gz: 24cf76930b63917dad1795a72113b9cf0646330671983b60bd367cc04d11d015
3
+ metadata.gz: 2a1dfcc57a6bae5c2e5bd01f7e6165218220f04258ac8fab3ab256b685454bb6
4
+ data.tar.gz: 724dbe5463d5bf223ef9652e566b448f38a4270d0131446e302236685ba43710
5
5
  SHA512:
6
- metadata.gz: 0e8acdf59a00128c3a68fb2db10864c0e0bf6a5e454aabceaf824cdf4ba62f8057f166da2dae0f17238a7a280137861ca4ba1da70b33165e6e890becb36be391
7
- data.tar.gz: 33178114bcbbda82ecf91cd27bfd7633621f6d7f2ea64fc3891596a8769cc80dac55ceab5c4fc75cd035d2a0188c9da2821743442cfcdbbce0cef2d3d479e6b5
6
+ metadata.gz: d21c5cce4267f7d5209e495415c90b21375686e1f8d65d9d2080d3d3aa2677344a48cbc1167e4e83bff6a27223e6f12d891a6416ad122b54ee0d36832dd08311
7
+ data.tar.gz: 8b0fd57ee957d8eb8d58d3556f7f4ab145a7eb5cac82286c2cefd439a77bc28f6e03abe38484154b308b59b3b30e69bd749fec968fe053ebb0b4f9560ae0eb17
data/README.md CHANGED
@@ -42,7 +42,7 @@ Here is a very high-level overview of how Hooks works:
42
42
  health_path: /health
43
43
  version_path: /version
44
44
 
45
- environment: development
45
+ environment: development # will be overridden by the RACK_ENV environment variable if set
46
46
  ```
47
47
 
48
48
  2. Then in your `config/endpoints` directory, you can define all your webhook endpoints in separate files. Here is an example of a simple endpoint configuration file:
@@ -153,17 +153,22 @@ This example will assume the following directory structure:
153
153
 
154
154
  ```text
155
155
  ├── config/
156
- │ ├── hooks.yml # global hooks config
157
- │ ├── puma.rb # puma config
156
+ │ ├── hooks.yml # global hooks config
157
+ │ ├── puma.rb # puma config
158
158
  │ └── endpoints/
159
159
  │ ├── hello.yml
160
160
  │ └── goodbye.yml
161
161
  └── plugins/
162
- ├── handlers/ # custom handler plugins
162
+ ├── handlers/ # custom handler plugins
163
163
  │ ├── hello_handler.rb
164
164
  │ └── goodbye_handler.rb
165
+ ├── lifecycle/ # custom lifecycle plugins (optional)
166
+ │ └── my_lifecycle_plugin.rb # custom lifecycle plugin (optional)
167
+ ├── instruments/ # custom instrument plugins (optional)
168
+ │ ├── stats.rb # a custom stats instrument plugin (optional)
169
+ │ └── failbot.rb # a custom error notifier instrument plugin (optional)
165
170
  └── auth/
166
- └── goodbye_auth.rb # custom auth plugin (optional)
171
+ └── goodbye_auth.rb # custom auth plugin (optional)
167
172
  ```
168
173
 
169
174
  Let's go through each step in detail.
@@ -174,8 +179,10 @@ First, create a `hooks.yml` file in the `config` directory. This file will defin
174
179
 
175
180
  ```yaml
176
181
  # file: config/hooks.yml
177
- handler_plugin_dir: ./plugins/handlers
178
- auth_plugin_dir: ./plugins/auth
182
+ handler_plugin_dir: ./plugins/handlers # Directory for handler plugins
183
+ auth_plugin_dir: ./plugins/auth # Directory for authentication plugins (optional)
184
+ lifecycle_plugin_dir: ./plugins/lifecycle # Directory for lifecycle plugins (optional)
185
+ instruments_plugin_dir: ./plugins/instruments # Directory for instrument plugins (optional)
179
186
 
180
187
  # Available endpoints
181
188
  # Each endpoint configuration file should be placed in the endpoints directory
@@ -189,7 +196,7 @@ health_path: /health
189
196
  version_path: /version
190
197
 
191
198
  # Runtime behavior
192
- environment: development # or production
199
+ environment: development # or production (will be overridden by the RACK_ENV environment variable if set)
193
200
  ```
194
201
 
195
202
  #### 2. Create your endpoint configurations
@@ -213,6 +220,10 @@ auth:
213
220
  type: Goodbye # This is a custom authentication plugin you would define in the plugins/auth
214
221
  secret_env_key: GOODBYE_API_KEY # the name of the environment variable containing the secret
215
222
  header: Authorization
223
+
224
+ # Optional additional options for the endpoint (can be anything you want)
225
+ opts:
226
+ foo: bar
216
227
  ```
217
228
 
218
229
  #### 3. Implement your handler plugins
@@ -251,6 +262,8 @@ class GoodbyeHandler < Hooks::Plugins::Handlers::Base
251
262
  end
252
263
  ```
253
264
 
265
+ See the [Handler Plugins](docs/handler_plugins.md) documentation for more information on how to create your own custom handler plugins and what the values of `payload`, `headers`, and `config` are when the `call` method is invoked.
266
+
254
267
  #### 4. Implement authentication plugins (optional)
255
268
 
256
269
  If you want to secure your webhook endpoints, you can create custom authentication plugins in the `plugins/auth` directory. Here is an example of a simple authentication plugin for the `/goodbye` endpoint:
@@ -268,7 +281,7 @@ module Hooks
268
281
  secret = fetch_secret(config, secret_env_key_name: :secret_env_key)
269
282
 
270
283
  # check if the Authorization header matches the secret
271
- auth_header = headers[config[:header]]
284
+ auth_header = headers[config[:auth][:header]]
272
285
  return false unless auth_header
273
286
 
274
287
  # compare the Authorization header with the secret
@@ -280,6 +293,8 @@ module Hooks
280
293
  end
281
294
  ```
282
295
 
296
+ To learn more about how you can create your own custom authentication plugins, see the [Auth Plugins](docs/auth_plugins.md) documentation.
297
+
283
298
  #### Summary
284
299
 
285
300
  What these steps have done is set up a Hooks server that listens for incoming webhook requests at `/webhooks/hello` and `/webhooks/goodbye`. The `/hello` endpoint will respond with a success message without any authentication, while the `/goodbye` endpoint will require a valid `Authorization` header that matches the secret defined in the environment variable `GOODBYE_API_KEY`. Before the `/goodbye` endpoint enters the defined handler, it will first check the authentication plugin to ensure the request is valid.
@@ -16,7 +16,7 @@ module Hooks
16
16
  root_path: "/webhooks",
17
17
  health_path: "/health",
18
18
  version_path: "/version",
19
- environment: "production",
19
+ environment: ENV.fetch("RACK_ENV", "production"),
20
20
  production: true,
21
21
  endpoints_dir: "./config/endpoints",
22
22
  use_catchall_route: false,
@@ -43,11 +43,30 @@ module Hooks
43
43
  secret = ENV[secret_env_key]
44
44
 
45
45
  if secret.nil? || !secret.is_a?(String) || secret.strip.empty?
46
- raise StandardError, "authentication configuration incomplete: missing secret value bound to #{secret_env_key_name}"
46
+ raise StandardError, "authentication configuration incomplete: missing secret value for environment variable"
47
47
  end
48
48
 
49
49
  return secret.strip
50
50
  end
51
+
52
+ # Find a header value by name with case-insensitive matching
53
+ #
54
+ # @param headers [Hash] HTTP headers from the request
55
+ # @param header_name [String] Name of the header to find
56
+ # @return [String, nil] The header value if found, nil otherwise
57
+ # @note This method performs case-insensitive header matching
58
+ def self.find_header_value(headers, header_name)
59
+ return nil unless headers.respond_to?(:each)
60
+ return nil if header_name.nil? || header_name.strip.empty?
61
+
62
+ target_header = header_name.downcase
63
+ headers.each do |key, value|
64
+ if key.to_s.downcase == target_header
65
+ return value.to_s
66
+ end
67
+ end
68
+ nil
69
+ end
51
70
  end
52
71
  end
53
72
  end
@@ -92,26 +92,32 @@ module Hooks
92
92
  validator_config = build_config(config)
93
93
 
94
94
  # Security: Check raw headers BEFORE normalization to detect tampering
95
- return false unless headers.respond_to?(:each)
95
+ unless headers.respond_to?(:each)
96
+ log.warn("Auth::HMAC validation failed: Invalid headers object")
97
+ return false
98
+ end
96
99
 
97
100
  signature_header = validator_config[:header]
98
101
 
99
- # Find the signature header with case-insensitive matching but preserve original value
100
- raw_signature = nil
101
- headers.each do |key, value|
102
- if key.to_s.downcase == signature_header.downcase
103
- raw_signature = value.to_s
104
- break
105
- end
106
- end
102
+ # Find the signature header with case-insensitive matching
103
+ raw_signature = find_header_value(headers, signature_header)
107
104
 
108
- return false if raw_signature.nil? || raw_signature.empty?
105
+ if raw_signature.nil? || raw_signature.empty?
106
+ log.warn("Auth::HMAC validation failed: Missing or empty signature header '#{signature_header}'")
107
+ return false
108
+ end
109
109
 
110
110
  # Security: Reject signatures with leading/trailing whitespace
111
- return false if raw_signature != raw_signature.strip
111
+ if raw_signature != raw_signature.strip
112
+ log.warn("Auth::HMAC validation failed: Signature contains leading/trailing whitespace")
113
+ return false
114
+ end
112
115
 
113
116
  # Security: Reject signatures containing null bytes or other control characters
114
- return false if raw_signature.match?(/[\u0000-\u001f\u007f-\u009f]/)
117
+ if raw_signature.match?(/[\u0000-\u001f\u007f-\u009f]/)
118
+ log.warn("Auth::HMAC validation failed: Signature contains control characters")
119
+ return false
120
+ end
115
121
 
116
122
  # Now we can safely normalize headers for the rest of the validation
117
123
  normalized_headers = normalize_headers(headers)
@@ -134,7 +140,13 @@ module Hooks
134
140
  )
135
141
 
136
142
  # Use secure comparison to prevent timing attacks
137
- Rack::Utils.secure_compare(computed_signature, provided_signature)
143
+ result = Rack::Utils.secure_compare(computed_signature, provided_signature)
144
+ if result
145
+ log.debug("Auth::HMAC validation successful for header '#{signature_header}'")
146
+ else
147
+ log.warn("Auth::HMAC validation failed: Signature mismatch")
148
+ end
149
+ result
138
150
  rescue StandardError => e
139
151
  log.error("Auth::HMAC validation failed: #{e.message}")
140
152
  false
@@ -62,37 +62,56 @@ module Hooks
62
62
  validator_config = build_config(config)
63
63
 
64
64
  # Security: Check raw headers BEFORE normalization to detect tampering
65
- return false unless headers.respond_to?(:each)
65
+ unless headers.respond_to?(:each)
66
+ log.warn("Auth::SharedSecret validation failed: Invalid headers object")
67
+ return false
68
+ end
66
69
 
67
70
  secret_header = validator_config[:header]
68
71
 
69
- # Find the secret header with case-insensitive matching but preserve original value
70
- raw_secret = nil
71
- headers.each do |key, value|
72
- if key.to_s.downcase == secret_header.downcase
73
- raw_secret = value.to_s
74
- break
75
- end
76
- end
72
+ # Find the secret header with case-insensitive matching
73
+ raw_secret = find_header_value(headers, secret_header)
77
74
 
78
- return false if raw_secret.nil? || raw_secret.empty?
75
+ if raw_secret.nil? || raw_secret.empty?
76
+ log.warn("Auth::SharedSecret validation failed: Missing or empty secret header '#{secret_header}'")
77
+ return false
78
+ end
79
79
 
80
80
  stripped_secret = raw_secret.strip
81
81
 
82
82
  # Security: Reject secrets with leading/trailing whitespace
83
- return false if raw_secret != stripped_secret
83
+ if raw_secret != stripped_secret
84
+ log.warn("Auth::SharedSecret validation failed: Secret contains leading/trailing whitespace")
85
+ return false
86
+ end
84
87
 
85
88
  # Security: Reject secrets containing null bytes or other control characters
86
- return false if raw_secret.match?(/[\u0000-\u001f\u007f-\u009f]/)
89
+ if raw_secret.match?(/[\u0000-\u001f\u007f-\u009f]/)
90
+ log.warn("Auth::SharedSecret validation failed: Secret contains control characters")
91
+ return false
92
+ end
87
93
 
88
94
  # Use secure comparison to prevent timing attacks
89
- Rack::Utils.secure_compare(secret, stripped_secret)
90
- rescue StandardError => _e
95
+ result = Rack::Utils.secure_compare(secret, stripped_secret)
96
+ if result
97
+ log.debug("Auth::SharedSecret validation successful for header '#{secret_header}'")
98
+ else
99
+ log.warn("Auth::SharedSecret validation failed: Signature mismatch")
100
+ end
101
+ result
102
+ rescue StandardError => e
103
+ log.error("Auth::SharedSecret validation failed: #{e.message}")
91
104
  false
92
105
  end
93
106
 
94
107
  private
95
108
 
109
+ # Short logger accessor for auth module
110
+ # @return [Hooks::Log] Logger instance
111
+ def self.log
112
+ Hooks::Log.instance
113
+ end
114
+
96
115
  # Build final configuration by merging defaults with provided config
97
116
  #
98
117
  # Combines default configuration values with user-provided settings,
data/lib/hooks/version.rb CHANGED
@@ -4,5 +4,5 @@
4
4
  module Hooks
5
5
  # Current version of the Hooks webhook framework
6
6
  # @return [String] The version string following semantic versioning
7
- VERSION = "0.0.6".freeze
7
+ VERSION = "0.1.0".freeze
8
8
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hooks-ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.6
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - github