Skip to content

Commit 81836b9

Browse files
authored
Merge pull request #54 from github/copilot/fix-53
Make handler field support only snake_case in configuration
2 parents 84dd5d9 + 9a08e7b commit 81836b9

39 files changed

+166
-115
lines changed

Gemfile.lock

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
PATH
22
remote: .
33
specs:
4-
hooks-ruby (0.2.1)
4+
hooks-ruby (0.3.0)
55
dry-schema (~> 1.14, >= 1.14.1)
66
grape (~> 2.3)
77
puma (~> 6.6)
@@ -148,15 +148,15 @@ GEM
148148
diff-lcs (>= 1.2.0, < 2.0)
149149
rspec-support (~> 3.13.0)
150150
rspec-support (3.13.4)
151-
rubocop (1.76.1)
151+
rubocop (1.76.2)
152152
json (~> 2.3)
153153
language_server-protocol (~> 3.17.0.2)
154154
lint_roller (~> 1.1.0)
155155
parallel (~> 1.10)
156156
parser (>= 3.3.0.2)
157157
rainbow (>= 2.2.2, < 4.0)
158158
regexp_parser (>= 2.9.3, < 3.0)
159-
rubocop-ast (>= 1.45.0, < 2.0)
159+
rubocop-ast (>= 1.45.1, < 2.0)
160160
ruby-progressbar (~> 1.7)
161161
unicode-display_width (>= 2.4.0, < 4.0)
162162
rubocop-ast (1.45.1)

README.md

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,11 @@ Here is a very high-level overview of how Hooks works:
5050
```yaml
5151
# file: config/endpoints/hello.yml
5252
path: /hello
53-
handler: MyCustomHandler # This is a custom handler plugin you would define in the plugins/handlers directory
53+
handler: my_custom_handler # This is a custom handler plugin you would define in the plugins/handlers directory (snake_case)
5454
```
5555

56+
> Note: If your handler's class name is `MyCustomHandler`, you would define it in the `plugins/handlers/my_custom_handler.rb` file. The `handler` field in the endpoint configuration file should be the snake_case version of the class name. So if your handler class is `MyCustomHandler`, you would use `my_custom_handler` in the endpoint configuration file.
57+
5658
3. Now create a corresponding handler plugin in the `plugins/handlers` directory. Here is an example of a simple handler plugin:
5759

5860
```ruby
@@ -64,7 +66,7 @@ Here is a very high-level overview of how Hooks works:
6466
# For this example, we will just return a success message
6567
{
6668
status: "success",
67-
handler: "MyCustomHandler",
69+
handler: "my_custom_handler",
6870
payload_received: payload,
6971
timestamp: Time.now.utc.iso8601
7072
}
@@ -208,16 +210,16 @@ Endpoint configurations are defined in the `config/endpoints` directory. Each en
208210
```yaml
209211
# file: config/endpoints/hello.yml
210212
path: /hello # becomes /webhooks/hello based on the root_path in hooks.yml
211-
handler: HelloHandler # This is a custom handler plugin you would define in the plugins/handlers
213+
handler: hello_handler # This is a custom handler plugin you would define in the plugins/handlers
212214
```
213215

214216
```yaml
215217
# file: config/endpoints/goodbye.yml
216218
path: /goodbye # becomes /webhooks/goodbye based on the root_path in hooks.yml
217-
handler: GoodbyeHandler # This is another custom handler plugin you would define in the plugins/handlers
219+
handler: goodbye_handler # This is another custom handler plugin you would define in the plugins/handlers
218220
219221
auth:
220-
type: Goodbye # This is a custom authentication plugin you would define in the plugins/auth
222+
type: goodbye # This is a custom authentication plugin you would define in the plugins/auth
221223
secret_env_key: GOODBYE_API_KEY # the name of the environment variable containing the secret
222224
header: Authorization
223225
@@ -255,7 +257,7 @@ class GoodbyeHandler < Hooks::Plugins::Handlers::Base
255257
# Ditto for the goodbye endpoint
256258
{
257259
message: "goodbye webhook processed successfully",
258-
handler: "GoodbyeHandler",
260+
handler: "goodbye_handler",
259261
timestamp: Time.now.utc.iso8601
260262
}
261263
end

docs/auth_plugins.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -448,3 +448,20 @@ module Hooks
448448
end
449449
end
450450
```
451+
452+
The configuration for this IP filtering plugin would look like this:
453+
454+
```yaml
455+
path: /example
456+
handler: CoolNewHandler # could be any handler you want to use
457+
458+
auth:
459+
type: ip_filtering_plugin # using the custom IP filtering plugin (remember IpFilteringPlugin becomes ip_filtering_plugin)
460+
461+
# You can specify additional options in the `opts` section but the `allowed_ips` option is required for this plugin demo to work
462+
opts:
463+
allowed_ips: # list of allowed IPs
464+
- "<ALLOWED_IP_1>"
465+
- "<ALLOWED_IP_2>"
466+
- "<ALLOWED_IP_3>"
467+
```

docs/handler_plugins.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ Handler plugins are Ruby classes that extend the `Hooks::Plugins::Handlers::Base
88

99
- `payload`: The webhook payload, which can be a Hash or a String. This is the data that the webhook sender sends to your endpoint.
1010
- `headers`: A Hash of HTTP headers that were sent with the webhook request.
11+
- `env`: A modified Rack environment that contains a lot of context about the request. This includes information about the request method, path, query parameters, and more. See [`rack_env_builder.rb`](../lib/hooks/app/rack_env_builder.rb) for the complete list of available keys.
1112
- `config`: A Hash containing the endpoint configuration. This can include any additional settings or parameters that you want to use in your handler. Most of the time, this won't be used but sometimes endpoint configs add `opts` that can be useful for the handler.
1213

1314
```ruby
@@ -28,6 +29,20 @@ class Example < Hooks::Plugins::Handlers::Base
2829
end
2930
```
3031

32+
After you write your own handler, it can be referenced in endpoint configuration files like so:
33+
34+
```yaml
35+
# example file path: config/endpoints/example.yml
36+
path: /example_webhook
37+
handler: example # this is the name of the handler plugin class
38+
```
39+
40+
It should be noted that the `handler:` key in the endpoint configuration file should match the name of the handler plugin class, but in lowercase and snake case. For example, if your handler plugin class is named `ExampleHandler`, the `handler:` key in the endpoint configuration file should be `example_handler`. Here are some more examples:
41+
42+
- `ExampleHandler` -> `example_handler`
43+
- `MyCustomHandler` -> `my_custom_handler`
44+
- `Cool2Handler` -> `cool_2_handler`
45+
3146
### `payload` Parameter
3247

3348
The `payload` parameter can be a Hash or a String. If the payload is a String, it will be parsed as JSON. If it is a Hash, it will be passed directly to the handler. The payload can contain any data that the webhook sender wants to send.

lib/hooks/app/api.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
require "json"
55
require "securerandom"
66
require_relative "helpers"
7+
#require_relative "network/ip_filtering"
78
require_relative "auth/auth"
89
require_relative "rack_env_builder"
910
require_relative "../plugins/handlers/base"
@@ -82,6 +83,13 @@ def self.create(config:, endpoints:, log:)
8283
plugin.on_request(rack_env)
8384
end
8485

86+
# TODO: IP filtering before processing the request if defined
87+
# If IP filtering is enabled at either global or endpoint level, run the filtering rules
88+
# before processing the request
89+
#if config[:ip_filtering] || endpoint_config[:ip_filtering]
90+
#ip_filtering!(headers, endpoint_config, config, request_context, rack_env)
91+
#end
92+
8593
enforce_request_limits(config, request_context)
8694
request.body.rewind
8795
raw_body = request.body.read

lib/hooks/app/helpers.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ def parse_payload(raw_body, headers, symbolize: false)
7474

7575
# Load handler class
7676
#
77-
# @param handler_class_name [String] The name of the handler class to load
77+
# @param handler_class_name [String] The name of the handler in snake_case (e.g., "github_handler")
7878
# @return [Object] An instance of the loaded handler class
7979
# @raise [StandardError] If handler cannot be found
8080
def load_handler(handler_class_name)

lib/hooks/core/config_validator.rb

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -125,11 +125,12 @@ def self.valid_handler_name?(handler_name)
125125
# Must not be empty or only whitespace
126126
return false if handler_name.strip.empty?
127127

128-
# Must match a safe pattern: alphanumeric + underscore, starting with uppercase
129-
return false unless handler_name.match?(/\A[A-Z][a-zA-Z0-9_]*\z/)
128+
# Must match strict snake_case pattern: starts with lowercase, no trailing/consecutive underscores
129+
return false unless handler_name.match?(/\A[a-z][a-z0-9]*(?:_[a-z0-9]+)*\z/)
130130

131-
# Must not be a system/built-in class name
132-
return false if Hooks::Security::DANGEROUS_CLASSES.include?(handler_name)
131+
# Convert to PascalCase for security check (since DANGEROUS_CLASSES uses PascalCase)
132+
pascal_case_name = handler_name.split("_").map(&:capitalize).join("")
133+
return false if Hooks::Security::DANGEROUS_CLASSES.include?(pascal_case_name)
133134

134135
true
135136
end

lib/hooks/core/plugin_loader.rb

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,11 +61,13 @@ def get_auth_plugin(plugin_name)
6161

6262
# Get handler plugin class by name
6363
#
64-
# @param handler_name [String] Name of the handler (e.g., "DefaultHandler", "Team1Handler")
64+
# @param handler_name [String] Name of the handler in snake_case (e.g., "github_handler", "team_1_handler")
6565
# @return [Class] The handler plugin class
6666
# @raise [StandardError] if handler not found
6767
def get_handler_plugin(handler_name)
68-
plugin_class = @handler_plugins[handler_name]
68+
# Convert snake_case to PascalCase for registry lookup
69+
pascal_case_name = handler_name.split("_").map(&:capitalize).join("")
70+
plugin_class = @handler_plugins[pascal_case_name]
6971

7072
unless plugin_class
7173
raise StandardError, "Handler plugin '#{handler_name}' not found. Available handlers: #{@handler_plugins.keys.join(', ')}"

lib/hooks/plugins/auth/base.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
require_relative "../../core/log"
55
require_relative "../../core/global_components"
66
require_relative "../../core/component_access"
7+
require_relative "timestamp_validator"
78

89
module Hooks
910
module Plugins
@@ -53,6 +54,13 @@ def self.fetch_secret(config, secret_env_key_name: :secret_env_key)
5354
return secret.strip
5455
end
5556

57+
# Get timestamp validator instance
58+
#
59+
# @return [TimestampValidator] Singleton timestamp validator instance
60+
def self.timestamp_validator
61+
TimestampValidator.new
62+
end
63+
5664
# Find a header value by name with case-insensitive matching
5765
#
5866
# @param headers [Hash] HTTP headers from the request

lib/hooks/plugins/auth/hmac.rb

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
require "openssl"
44
require "time"
55
require_relative "base"
6-
require_relative "timestamp_validator"
76

87
module Hooks
98
module Plugins
@@ -271,14 +270,6 @@ def self.valid_timestamp?(headers, config)
271270
timestamp_validator.valid?(timestamp_value, tolerance)
272271
end
273272

274-
# Get timestamp validator instance
275-
#
276-
# @return [TimestampValidator] Singleton timestamp validator instance
277-
# @api private
278-
def self.timestamp_validator
279-
@timestamp_validator ||= TimestampValidator.new
280-
end
281-
282273
# Compute HMAC signature based on configuration requirements
283274
#
284275
# Generates the expected HMAC signature for the given payload using the

0 commit comments

Comments
 (0)