|
2 | 2 |
|
3 | 3 | require "securerandom" |
4 | 4 | require_relative "../security" |
| 5 | +require_relative "../core/plugin_loader" |
5 | 6 |
|
6 | 7 | module Hooks |
7 | 8 | module App |
@@ -64,131 +65,20 @@ def parse_payload(raw_body, headers, symbolize: true) |
64 | 65 | # Load handler class |
65 | 66 | # |
66 | 67 | # @param handler_class_name [String] The name of the handler class to load |
67 | | - # @param handler_dir [String] The directory containing handler files |
68 | 68 | # @return [Object] An instance of the loaded handler class |
69 | | - # @raise [LoadError] If the handler file or class cannot be found |
70 | | - # @raise [StandardError] Halts with error if handler cannot be loaded |
71 | | - def load_handler(handler_class_name, handler_dir) |
72 | | - # Security: Validate handler class name to prevent arbitrary class loading |
73 | | - unless valid_handler_class_name?(handler_class_name) |
74 | | - error!("invalid handler class name: #{handler_class_name}", 400) |
| 69 | + # @raise [StandardError] If handler cannot be found |
| 70 | + def load_handler(handler_class_name) |
| 71 | + # Get handler class from loaded plugins registry (boot-time loaded only) |
| 72 | + begin |
| 73 | + handler_class = Core::PluginLoader.get_handler_plugin(handler_class_name) |
| 74 | + return handler_class.new |
| 75 | + rescue => e |
| 76 | + error!("failed to get handler '#{handler_class_name}': #{e.message}", 500) |
75 | 77 | end |
76 | | - |
77 | | - # Convert class name to file name (e.g., Team1Handler -> team1_handler.rb) |
78 | | - # E.g.2: GithubHandler -> github_handler.rb |
79 | | - # E.g.3: GitHubHandler -> git_hub_handler.rb |
80 | | - file_name = handler_class_name.gsub(/([A-Z])/, '_\1').downcase.sub(/^_/, "") + ".rb" |
81 | | - file_path = File.join(handler_dir, file_name) |
82 | | - |
83 | | - # Security: Ensure the file path doesn't escape the handler directory |
84 | | - normalized_handler_dir = Pathname.new(File.expand_path(handler_dir)) |
85 | | - normalized_file_path = Pathname.new(File.expand_path(file_path)) |
86 | | - unless normalized_file_path.descend.any? { |path| path == normalized_handler_dir } |
87 | | - error!("handler path outside of handler directory", 400) |
88 | | - end |
89 | | - |
90 | | - if File.exist?(file_path) |
91 | | - require file_path |
92 | | - handler_class = Object.const_get(handler_class_name) |
93 | | - |
94 | | - # Security: Ensure the loaded class inherits from the expected base class |
95 | | - unless handler_class < Hooks::Plugins::Handlers::Base |
96 | | - error!("handler class must inherit from Hooks::Plugins::Handlers::Base", 400) |
97 | | - end |
98 | | - |
99 | | - handler_class.new |
100 | | - else |
101 | | - raise LoadError, "Handler #{handler_class_name} not found at #{file_path}" |
102 | | - end |
103 | | - rescue => e |
104 | | - error!("failed to load handler: #{e.message}", 500) |
105 | | - end |
106 | | - |
107 | | - # Load auth plugin class |
108 | | - # |
109 | | - # @param auth_plugin_class_name [String] The name of the auth plugin class to load |
110 | | - # @param auth_plugin_dir [String] The directory containing auth plugin files |
111 | | - # @return [Class] The loaded auth plugin class |
112 | | - # @raise [LoadError] If the auth plugin file or class cannot be found |
113 | | - # @raise [StandardError] Halts with error if auth plugin cannot be loaded |
114 | | - def load_auth_plugin(auth_plugin_class_name, auth_plugin_dir) |
115 | | - # Security: Validate auth plugin class name to prevent arbitrary class loading |
116 | | - unless valid_auth_plugin_class_name?(auth_plugin_class_name) |
117 | | - error!("invalid auth plugin class name: #{auth_plugin_class_name}", 400) |
118 | | - end |
119 | | - |
120 | | - # Convert class name to file name (e.g., SomeCoolAuthPlugin -> some_cool_auth_plugin.rb) |
121 | | - file_name = auth_plugin_class_name.gsub(/([A-Z])/, '_\1').downcase.sub(/^_/, "") + ".rb" |
122 | | - file_path = File.join(auth_plugin_dir, file_name) |
123 | | - |
124 | | - # Security: Ensure the file path doesn't escape the auth plugin directory |
125 | | - normalized_auth_plugin_dir = Pathname.new(File.expand_path(auth_plugin_dir)) |
126 | | - normalized_file_path = Pathname.new(File.expand_path(file_path)) |
127 | | - unless normalized_file_path.descend.any? { |path| path == normalized_auth_plugin_dir } |
128 | | - error!("auth plugin path outside of auth plugin directory", 400) |
129 | | - end |
130 | | - |
131 | | - if File.exist?(file_path) |
132 | | - require file_path |
133 | | - auth_plugin_class = Object.const_get("Hooks::Plugins::Auth::#{auth_plugin_class_name}") |
134 | | - |
135 | | - # Security: Ensure the loaded class inherits from the expected base class |
136 | | - unless auth_plugin_class < Hooks::Plugins::Auth::Base |
137 | | - error!("auth plugin class must inherit from Hooks::Plugins::Auth::Base", 400) |
138 | | - end |
139 | | - |
140 | | - auth_plugin_class |
141 | | - else |
142 | | - error!("Auth plugin #{auth_plugin_class_name} not found at #{file_path}", 500) |
143 | | - end |
144 | | - rescue => e |
145 | | - error!("failed to load auth plugin: #{e.message}", 500) |
146 | 78 | end |
147 | 79 |
|
148 | 80 | private |
149 | 81 |
|
150 | | - # Validate that a handler class name is safe to load |
151 | | - # |
152 | | - # @param class_name [String] The class name to validate |
153 | | - # @return [Boolean] true if the class name is safe, false otherwise |
154 | | - def valid_handler_class_name?(class_name) |
155 | | - # Must be a string |
156 | | - return false unless class_name.is_a?(String) |
157 | | - |
158 | | - # Must not be empty or only whitespace |
159 | | - return false if class_name.strip.empty? |
160 | | - |
161 | | - # Must match a safe pattern: alphanumeric + underscore, starting with uppercase |
162 | | - # Examples: MyHandler, GitHubHandler, Team1Handler |
163 | | - return false unless class_name.match?(/\A[A-Z][a-zA-Z0-9_]*\z/) |
164 | | - |
165 | | - # Must not be a system/built-in class name |
166 | | - return false if Hooks::Security::DANGEROUS_CLASSES.include?(class_name) |
167 | | - |
168 | | - true |
169 | | - end |
170 | | - |
171 | | - # Validate that an auth plugin class name is safe to load |
172 | | - # |
173 | | - # @param class_name [String] The class name to validate |
174 | | - # @return [Boolean] true if the class name is safe, false otherwise |
175 | | - def valid_auth_plugin_class_name?(class_name) |
176 | | - # Must be a string |
177 | | - return false unless class_name.is_a?(String) |
178 | | - |
179 | | - # Must not be empty or only whitespace |
180 | | - return false if class_name.strip.empty? |
181 | | - |
182 | | - # Must match a safe pattern: alphanumeric + underscore, starting with uppercase |
183 | | - # Examples: MyAuthPlugin, SomeCoolAuthPlugin, CustomAuth |
184 | | - return false unless class_name.match?(/\A[A-Z][a-zA-Z0-9_]*\z/) |
185 | | - |
186 | | - # Must not be a system/built-in class name |
187 | | - return false if Hooks::Security::DANGEROUS_CLASSES.include?(class_name) |
188 | | - |
189 | | - true |
190 | | - end |
191 | | - |
192 | 82 | # Determine HTTP error code from exception |
193 | 83 | # |
194 | 84 | # @param exception [Exception] The exception to map to an HTTP status code |
|
0 commit comments