Skip to content

Commit 363c0b4

Browse files
committed
Enhance IP filtering documentation with detailed method descriptions and examples
1 parent eaa06e4 commit 363c0b4

File tree

1 file changed

+179
-21
lines changed

1 file changed

+179
-21
lines changed

lib/hooks/app/network/ip_filtering.rb

Lines changed: 179 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -6,30 +6,71 @@
66
module Hooks
77
module App
88
module Network
9-
# Application-level IP filtering functionality
10-
# Provides both allowlist and blocklist filtering with CIDR support
9+
# Application-level IP filtering functionality for HTTP requests.
10+
#
11+
# This class provides robust IP filtering capabilities supporting both allowlist
12+
# and blocklist filtering with CIDR notation support. It can extract client IP
13+
# addresses from various HTTP headers and validate them against configured rules.
14+
#
15+
# The filtering logic follows these rules:
16+
# 1. If a blocklist is configured and the IP matches, access is denied
17+
# 2. If an allowlist is configured, the IP must match to be allowed
18+
# 3. If no allowlist is configured and IP is not blocked, access is allowed
19+
#
20+
# @example Basic usage with endpoint configuration
21+
# config = {
22+
# ip_filtering: {
23+
# allowlist: ["192.168.1.0/24", "10.0.0.1"],
24+
# blocklist: ["192.168.1.100"],
25+
# ip_header: "X-Real-IP"
26+
# }
27+
# }
28+
# IpFiltering.ip_filtering!(headers, config, {}, {}, env)
29+
#
30+
# @note This class is designed to work with Rack-based applications and
31+
# expects headers to be in a Hash format.
1132
class IpFiltering
12-
# Default IP header to check for client IP
33+
# Default HTTP header to check for client IP address.
34+
# @return [String] the default header name
1335
DEFAULT_IP_HEADER = "X-Forwarded-For"
1436

15-
# Verifies the incoming request passes the configured IP filtering rules.
16-
#
17-
# This method assumes that the client IP address is available in the request headers (e.g., `X-Forwarded-For`).
18-
# The headers that is used is configurable via the endpoint configuration.
19-
# It checks the IP address against the allowed and denied lists defined in the endpoint configuration.
20-
# If the IP address is not allowed, it instantly returns an error response via the `error!` method.
21-
# If the IP filtering configuration is missing or invalid, it raises an error.
22-
# If IP filtering is configured at the global level, it will also check against the global configuration first,
23-
# and then against the endpoint-specific configuration.
24-
#
25-
# @param headers [Hash] The request headers.
26-
# @param endpoint_config [Hash] The endpoint configuration, must include :ip_filtering key.
27-
# @param global_config [Hash] The global configuration (optional, for compatibility).
28-
# @param request_context [Hash] Context for the request, e.g. request ID, path, handler (optional).
29-
# @param env [Hash] The Rack environment
30-
# @raise [StandardError] Raises error if IP filtering fails or is misconfigured.
31-
# @return [void]
32-
# @note This method will halt execution with an error if IP filtering rules fail.
37+
# Verifies that an incoming request passes the configured IP filtering rules.
38+
#
39+
# This method extracts the client IP address from request headers and validates
40+
# it against configured allowlist and blocklist rules. The method will halt
41+
# execution by raising an error if the IP filtering rules fail.
42+
#
43+
# The IP filtering configuration can be defined at both global and endpoint levels,
44+
# with endpoint configuration taking precedence. If no IP filtering is configured,
45+
# the method returns early without performing any checks.
46+
#
47+
# The client IP is extracted from HTTP headers, with support for configurable
48+
# header names. The default header is X-Forwarded-For, which can contain multiple
49+
# comma-separated IPs (the first IP is used as the original client).
50+
#
51+
# @param headers [Hash] The request headers as key-value pairs
52+
# @param endpoint_config [Hash] The endpoint-specific configuration containing :ip_filtering
53+
# @param global_config [Hash] The global configuration (optional, for compatibility)
54+
# @param request_context [Hash] Context information for the request (e.g., request_id, path, handler)
55+
# @param env [Hash] The Rack environment hash
56+
#
57+
# @raise [Hooks::Plugins::Handlers::Error] Raises a 403 error if IP filtering rules fail
58+
# @return [void] Returns nothing if IP filtering passes or is not configured
59+
#
60+
# @example Successful IP filtering
61+
# headers = { "X-Forwarded-For" => "192.168.1.50" }
62+
# config = { ip_filtering: { allowlist: ["192.168.1.0/24"] } }
63+
# IpFiltering.ip_filtering!(headers, config, {}, { request_id: "123" }, env)
64+
#
65+
# @example IP filtering failure
66+
# headers = { "X-Forwarded-For" => "10.0.0.1" }
67+
# config = { ip_filtering: { allowlist: ["192.168.1.0/24"] } }
68+
# # Raises Hooks::Plugins::Handlers::Error with 403 status
69+
# IpFiltering.ip_filtering!(headers, config, {}, { request_id: "123" }, env)
70+
#
71+
# @note This method assumes that the client IP address is available in the request headers
72+
# @note If the IP filtering configuration is missing or invalid, it raises an error
73+
# @note This method will halt execution with an error if IP filtering rules fail
3374
def self.ip_filtering!(headers, endpoint_config, global_config, request_context, env)
3475
# Determine which IP filtering configuration to use
3576
ip_config = resolve_ip_config(endpoint_config, global_config)
@@ -51,11 +92,62 @@ def self.ip_filtering!(headers, endpoint_config, global_config, request_context,
5192
end
5293
end
5394

95+
# Resolves the IP filtering configuration to use for the current request.
96+
#
97+
# This method determines which IP filtering configuration should be applied
98+
# by checking endpoint-specific configuration first, then falling back to
99+
# global configuration. This allows for flexible configuration inheritance
100+
# with endpoint-level overrides.
101+
#
102+
# @param endpoint_config [Hash] The endpoint-specific configuration
103+
# @param global_config [Hash] The global application configuration
104+
#
105+
# @return [Hash, nil] The IP filtering configuration hash, or nil if none configured
106+
#
107+
# @example With endpoint configuration
108+
# endpoint_config = { ip_filtering: { allowlist: ["192.168.1.0/24"] } }
109+
# global_config = { ip_filtering: { allowlist: ["10.0.0.0/8"] } }
110+
# resolve_ip_config(endpoint_config, global_config)
111+
# # => { allowlist: ["192.168.1.0/24"] }
112+
#
113+
# @example With only global configuration
114+
# endpoint_config = {}
115+
# global_config = { ip_filtering: { allowlist: ["10.0.0.0/8"] } }
116+
# resolve_ip_config(endpoint_config, global_config)
117+
# # => { allowlist: ["10.0.0.0/8"] }
118+
#
119+
# @note Endpoint-level configuration takes precedence over global configuration
54120
private_class_method def self.resolve_ip_config(endpoint_config, global_config)
55121
# Endpoint-level configuration takes precedence over global configuration
56122
endpoint_config[:ip_filtering] || global_config[:ip_filtering]
57123
end
58124

125+
# Extracts the client IP address from request headers.
126+
#
127+
# This method looks for the client IP in the specified header (or default
128+
# X-Forwarded-For header). It performs case-insensitive header matching
129+
# and handles comma-separated IP lists by taking the first IP address,
130+
# which represents the original client in proxy chains.
131+
#
132+
# @param headers [Hash] The request headers as key-value pairs
133+
# @param ip_config [Hash] The IP filtering configuration containing :ip_header
134+
#
135+
# @return [String, nil] The client IP address, or nil if not found or empty
136+
#
137+
# @example Extracting from X-Forwarded-For
138+
# headers = { "X-Forwarded-For" => "192.168.1.50, 10.0.0.1" }
139+
# ip_config = { ip_header: "X-Forwarded-For" }
140+
# extract_client_ip(headers, ip_config)
141+
# # => "192.168.1.50"
142+
#
143+
# @example Extracting from custom header
144+
# headers = { "X-Real-IP" => "203.0.113.45" }
145+
# ip_config = { ip_header: "X-Real-IP" }
146+
# extract_client_ip(headers, ip_config)
147+
# # => "203.0.113.45"
148+
#
149+
# @note Case-insensitive header lookup is performed
150+
# @note For comma-separated IP lists, only the first IP is returned
59151
private_class_method def self.extract_client_ip(headers, ip_config)
60152
# Use configured header or default to X-Forwarded-For
61153
ip_header = ip_config[:ip_header] || DEFAULT_IP_HEADER
@@ -72,6 +164,40 @@ def self.ip_filtering!(headers, endpoint_config, global_config, request_context,
72164
nil
73165
end
74166

167+
# Determines if a client IP address is allowed based on filtering rules.
168+
#
169+
# This method implements the core IP filtering logic by checking the client
170+
# IP against configured blocklist and allowlist rules. The filtering follows
171+
# these precedence rules:
172+
# 1. If blocklist exists and IP matches, deny access (return false)
173+
# 2. If allowlist exists, IP must match to be allowed (return true/false)
174+
# 3. If no allowlist exists and IP not blocked, allow access (return true)
175+
#
176+
# @param client_ip [String] The client IP address to validate
177+
# @param ip_config [Hash] The IP filtering configuration containing :blocklist and/or :allowlist
178+
#
179+
# @return [Boolean] true if IP is allowed, false if blocked or invalid
180+
#
181+
# @example IP allowed by allowlist
182+
# client_ip = "192.168.1.50"
183+
# ip_config = { allowlist: ["192.168.1.0/24"] }
184+
# ip_allowed?(client_ip, ip_config)
185+
# # => true
186+
#
187+
# @example IP blocked by blocklist
188+
# client_ip = "192.168.1.100"
189+
# ip_config = { blocklist: ["192.168.1.100"] }
190+
# ip_allowed?(client_ip, ip_config)
191+
# # => false
192+
#
193+
# @example Invalid IP format
194+
# client_ip = "invalid-ip"
195+
# ip_config = { allowlist: ["192.168.1.0/24"] }
196+
# ip_allowed?(client_ip, ip_config)
197+
# # => false
198+
#
199+
# @note Invalid IP addresses are automatically denied
200+
# @note Blocklist rules take precedence over allowlist rules
75201
private_class_method def self.ip_allowed?(client_ip, ip_config)
76202
# Parse client IP
77203
begin
@@ -94,6 +220,38 @@ def self.ip_filtering!(headers, endpoint_config, global_config, request_context,
94220
true
95221
end
96222

223+
# Checks if a client IP address matches any pattern in an IP list.
224+
#
225+
# This method iterates through a list of IP patterns (which can include
226+
# individual IPs or CIDR ranges) and determines if the client IP matches
227+
# any of them. It uses Ruby's IPAddr class for robust IP address and
228+
# CIDR range matching, with error handling for invalid IP patterns.
229+
#
230+
# @param client_addr [IPAddr] The client IP address as an IPAddr object
231+
# @param ip_list [Array<String>] Array of IP patterns (IPs or CIDR ranges)
232+
#
233+
# @return [Boolean] true if client IP matches any pattern in the list, false otherwise
234+
#
235+
# @example Matching individual IP
236+
# client_addr = IPAddr.new("192.168.1.50")
237+
# ip_list = ["192.168.1.50", "10.0.0.1"]
238+
# ip_matches_list?(client_addr, ip_list)
239+
# # => true
240+
#
241+
# @example Matching CIDR range
242+
# client_addr = IPAddr.new("192.168.1.50")
243+
# ip_list = ["192.168.1.0/24", "10.0.0.0/8"]
244+
# ip_matches_list?(client_addr, ip_list)
245+
# # => true
246+
#
247+
# @example No match found
248+
# client_addr = IPAddr.new("203.0.113.45")
249+
# ip_list = ["192.168.1.0/24", "10.0.0.0/8"]
250+
# ip_matches_list?(client_addr, ip_list)
251+
# # => false
252+
#
253+
# @note Invalid IP patterns in the list are silently skipped
254+
# @note Supports both IPv4 and IPv6 addresses and ranges
97255
private_class_method def self.ip_matches_list?(client_addr, ip_list)
98256
ip_list.each do |ip_pattern|
99257
begin

0 commit comments

Comments
 (0)