otto 2.0.0.pre9 → 2.0.0.pre10

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.
@@ -1,14 +1,25 @@
1
- # lib/otto/helpers/request.rb
1
+ # lib/otto/request.rb
2
2
  #
3
3
  # frozen_string_literal: true
4
4
 
5
- require_relative 'base'
5
+ require 'rack/request'
6
6
 
7
7
  class Otto
8
- # Request helper methods providing HTTP request handling utilities
9
- module RequestHelpers
10
- include Otto::BaseHelpers
11
-
8
+ # Otto's enhanced Rack::Request class with built-in helpers
9
+ #
10
+ # This class extends Rack::Request with Otto's framework helpers for
11
+ # HTTP request handling, privacy, security, and locale management.
12
+ # Projects can register additional helpers via Otto#register_request_helpers.
13
+ #
14
+ # @example Using Otto's request in route handlers
15
+ # def show(req, res)
16
+ # req.masked_ip # Privacy-safe masked IP
17
+ # req.geo_country # ISO country code
18
+ # req.check_locale! # Set locale for request
19
+ # end
20
+ #
21
+ # @see Otto#register_request_helpers
22
+ class Request < Rack::Request
12
23
  def user_agent
13
24
  env['HTTP_USER_AGENT']
14
25
  end
@@ -1,14 +1,27 @@
1
- # lib/otto/helpers/response.rb
1
+ # lib/otto/response.rb
2
2
  #
3
3
  # frozen_string_literal: true
4
4
 
5
- require_relative 'base'
5
+ require 'rack/response'
6
6
 
7
7
  class Otto
8
- # Response helper methods providing HTTP response handling utilities
9
- module ResponseHelpers
10
- include Otto::BaseHelpers
11
-
8
+ # Otto's enhanced Rack::Response class with built-in helpers
9
+ #
10
+ # This class extends Rack::Response with Otto's framework helpers for
11
+ # HTTP response handling, cookie management, CSP headers, and security.
12
+ # Projects can register additional helpers via Otto#register_response_helpers.
13
+ #
14
+ # @example Using Otto's response in route handlers
15
+ # def show(req, res)
16
+ # res.send_secure_cookie('session_id', token, 3600)
17
+ # res.send_csp_headers('text/html', nonce)
18
+ # res.no_cache!
19
+ # end
20
+ #
21
+ # @see Otto#register_response_helpers
22
+ class Response < Rack::Response
23
+ # Reference to the request object (needed by some response helpers)
24
+ # @return [Otto::Request]
12
25
  attr_accessor :request
13
26
 
14
27
  def send_secure_cookie(name, value, ttl, opts = {})
@@ -171,7 +184,7 @@ class Otto
171
184
  # # => "/myapp/admin/settings"
172
185
  def app_path(*paths)
173
186
  paths = paths.flatten.compact
174
- paths.unshift(request.env['SCRIPT_NAME']) if request.env['SCRIPT_NAME']
187
+ paths.unshift(request.env['SCRIPT_NAME']) if request&.env&.[]('SCRIPT_NAME')
175
188
  paths.join('/').gsub('//', '/')
176
189
  end
177
190
  end
data/lib/otto/route.rb CHANGED
@@ -100,10 +100,8 @@ class Otto
100
100
  # @return [Array] Rack response array [status, headers, body]
101
101
  def call(env, extra_params = {})
102
102
  extra_params ||= {}
103
- req = Rack::Request.new(env)
104
- res = Rack::Response.new
105
- req.extend Otto::RequestHelpers
106
- res.extend Otto::ResponseHelpers
103
+ req = otto.request_class.new(env)
104
+ res = otto.response_class.new
107
105
  res.request = req
108
106
 
109
107
  # Make security config available to response helpers
@@ -23,8 +23,8 @@ class Otto
23
23
  # @return [Array] Rack response array
24
24
  def call(env, extra_params = {})
25
25
  @start_time = Otto::Utils.now_in_μs
26
- req = Rack::Request.new(env)
27
- res = Rack::Response.new
26
+ req = otto_instance ? otto_instance.request_class.new(env) : Otto::Request.new(env)
27
+ res = otto_instance ? otto_instance.response_class.new : Otto::Response.new
28
28
 
29
29
  begin
30
30
  setup_request_response(req, res, env, extra_params)
@@ -118,14 +118,12 @@ class Otto
118
118
  end
119
119
 
120
120
  # Setup request and response with the same extensions and processing as Route#call
121
- # @param req [Rack::Request] Request object
122
- # @param res [Rack::Response] Response object
121
+ # @param req [Otto::Request] Request object
122
+ # @param res [Otto::Response] Response object
123
123
  # @param env [Hash] Rack environment
124
124
  # @param extra_params [Hash] Additional parameters
125
125
  def setup_request_response(req, res, env, extra_params)
126
- # Apply the same extensions as original Route#call
127
- req.extend Otto::RequestHelpers
128
- res.extend Otto::ResponseHelpers
126
+ # Set request reference (helpers are already included in class)
129
127
  res.request = req
130
128
 
131
129
  # Make security config available to response helpers
@@ -11,8 +11,8 @@ class Otto
11
11
  class LambdaHandler < BaseHandler
12
12
  def call(env, extra_params = {})
13
13
  start_time = Otto::Utils.now_in_μs
14
- req = Rack::Request.new(env)
15
- res = Rack::Response.new
14
+ req = otto_instance ? otto_instance.request_class.new(env) : Otto::Request.new(env)
15
+ res = otto_instance ? otto_instance.response_class.new : Otto::Response.new
16
16
 
17
17
  begin
18
18
  # Security: Lambda handlers require pre-configured procs from Otto instance
@@ -21,7 +21,7 @@ class Otto
21
21
  api_key = env["HTTP_#{@header_name.upcase.tr('-', '_')}"]
22
22
 
23
23
  if api_key.nil?
24
- request = Rack::Request.new(env)
24
+ request = Otto::Request.new(env)
25
25
  api_key = request.params[@param_name]
26
26
  end
27
27
 
@@ -0,0 +1,167 @@
1
+ # lib/otto/security/core.rb
2
+ #
3
+ # frozen_string_literal: true
4
+
5
+ class Otto
6
+ module Security
7
+ # Core security configuration methods included in the Otto class.
8
+ # Provides the public API for enabling and configuring security features.
9
+ module Core
10
+ # Enable CSRF protection for POST, PUT, DELETE, and PATCH requests.
11
+ # This will automatically add CSRF tokens to HTML forms and validate
12
+ # them on unsafe HTTP methods.
13
+ #
14
+ # @example
15
+ # otto.enable_csrf_protection!
16
+ def enable_csrf_protection!
17
+ ensure_not_frozen!
18
+ return if @middleware.includes?(Otto::Security::Middleware::CSRFMiddleware)
19
+
20
+ @security_config.enable_csrf_protection!
21
+ use Otto::Security::Middleware::CSRFMiddleware
22
+ end
23
+
24
+ # Enable request validation including input sanitization, size limits,
25
+ # and protection against XSS and SQL injection attacks.
26
+ #
27
+ # @example
28
+ # otto.enable_request_validation!
29
+ def enable_request_validation!
30
+ ensure_not_frozen!
31
+ return if @middleware.includes?(Otto::Security::Middleware::ValidationMiddleware)
32
+
33
+ @security_config.input_validation = true
34
+ use Otto::Security::Middleware::ValidationMiddleware
35
+ end
36
+
37
+ # Enable rate limiting to protect against abuse and DDoS attacks.
38
+ # This will automatically add rate limiting rules based on client IP.
39
+ #
40
+ # @param options [Hash] Rate limiting configuration options
41
+ # @option options [Integer] :requests_per_minute Maximum requests per minute per IP (default: 100)
42
+ # @option options [Hash] :custom_rules Custom rate limiting rules
43
+ # @example
44
+ # otto.enable_rate_limiting!(requests_per_minute: 50)
45
+ def enable_rate_limiting!(options = {})
46
+ ensure_not_frozen!
47
+ return if @middleware.includes?(Otto::Security::Middleware::RateLimitMiddleware)
48
+
49
+ @security.configure_rate_limiting(options)
50
+ use Otto::Security::Middleware::RateLimitMiddleware
51
+ end
52
+
53
+ # Add a custom rate limiting rule.
54
+ #
55
+ # @param name [String, Symbol] Rule name
56
+ # @param options [Hash] Rule configuration
57
+ # @option options [Integer] :limit Maximum requests
58
+ # @option options [Integer] :period Time period in seconds (default: 60)
59
+ # @option options [Proc] :condition Optional condition proc that receives request
60
+ # @example
61
+ # otto.add_rate_limit_rule('uploads', limit: 5, period: 300, condition: ->(req) { req.post? && req.path.include?('upload') })
62
+ def add_rate_limit_rule(name, options)
63
+ ensure_not_frozen!
64
+ @security_config.rate_limiting_config[:custom_rules][name.to_s] = options
65
+ end
66
+
67
+ # Add a trusted proxy server for accurate client IP detection.
68
+ # Only requests from trusted proxies will have their forwarded headers honored.
69
+ #
70
+ # @param proxy [String, Regexp] IP address, CIDR range, or regex pattern
71
+ # @example
72
+ # otto.add_trusted_proxy('10.0.0.0/8')
73
+ # otto.add_trusted_proxy(/^172\.16\./)
74
+ def add_trusted_proxy(proxy)
75
+ ensure_not_frozen!
76
+ @security_config.add_trusted_proxy(proxy)
77
+ end
78
+
79
+ # Set custom security headers that will be added to all responses.
80
+ # These merge with the default security headers.
81
+ #
82
+ # @param headers [Hash] Hash of header name => value pairs
83
+ # @example
84
+ # otto.set_security_headers({
85
+ # 'content-security-policy' => "default-src 'self'",
86
+ # 'strict-transport-security' => 'max-age=31536000'
87
+ # })
88
+ def set_security_headers(headers)
89
+ ensure_not_frozen!
90
+ @security_config.security_headers.merge!(headers)
91
+ end
92
+
93
+ # Enable HTTP Strict Transport Security (HSTS) header.
94
+ # WARNING: This can make your domain inaccessible if HTTPS is not properly
95
+ # configured. Only enable this when you're certain HTTPS is working correctly.
96
+ #
97
+ # @param max_age [Integer] Maximum age in seconds (default: 1 year)
98
+ # @param include_subdomains [Boolean] Apply to all subdomains (default: true)
99
+ # @example
100
+ # otto.enable_hsts!(max_age: 86400, include_subdomains: false)
101
+ def enable_hsts!(max_age: 31_536_000, include_subdomains: true)
102
+ ensure_not_frozen!
103
+ @security_config.enable_hsts!(max_age: max_age, include_subdomains: include_subdomains)
104
+ end
105
+
106
+ # Enable Content Security Policy (CSP) header to prevent XSS attacks.
107
+ # The default policy only allows resources from the same origin.
108
+ #
109
+ # @param policy [String] CSP policy string (default: "default-src 'self'")
110
+ # @example
111
+ # otto.enable_csp!("default-src 'self'; script-src 'self' 'unsafe-inline'")
112
+ def enable_csp!(policy = "default-src 'self'")
113
+ ensure_not_frozen!
114
+ @security_config.enable_csp!(policy)
115
+ end
116
+
117
+ # Enable X-Frame-Options header to prevent clickjacking attacks.
118
+ #
119
+ # @param option [String] Frame options: 'DENY', 'SAMEORIGIN', or 'ALLOW-FROM uri'
120
+ # @example
121
+ # otto.enable_frame_protection!('DENY')
122
+ def enable_frame_protection!(option = 'SAMEORIGIN')
123
+ ensure_not_frozen!
124
+ @security_config.enable_frame_protection!(option)
125
+ end
126
+
127
+ # Enable Content Security Policy (CSP) with nonce support for dynamic header generation.
128
+ # This enables the res.send_csp_headers response helper method.
129
+ #
130
+ # @param debug [Boolean] Enable debug logging for CSP headers (default: false)
131
+ # @example
132
+ # otto.enable_csp_with_nonce!(debug: true)
133
+ def enable_csp_with_nonce!(debug: false)
134
+ ensure_not_frozen!
135
+ @security_config.enable_csp_with_nonce!(debug: debug)
136
+ end
137
+
138
+ # Add an authentication strategy with a registered name
139
+ #
140
+ # This is the primary public API for registering authentication strategies.
141
+ # The name you provide here will be available as `strategy_result.strategy_name`
142
+ # in your application code, making it easy to identify which strategy authenticated
143
+ # the current request.
144
+ #
145
+ # Also available via Otto::Security::Configurator for consolidated security config.
146
+ #
147
+ # @param name [String, Symbol] Strategy name (e.g., 'session', 'api_key', 'jwt')
148
+ # @param strategy [AuthStrategy] Strategy instance
149
+ # @example
150
+ # otto.add_auth_strategy('session', SessionStrategy.new(session_key: 'user_id'))
151
+ # otto.add_auth_strategy('api_key', APIKeyStrategy.new)
152
+ # @raise [ArgumentError] if strategy name already registered
153
+ def add_auth_strategy(name, strategy)
154
+ ensure_not_frozen!
155
+ # Ensure auth_config is initialized (handles edge case where it might be nil)
156
+ @auth_config = { auth_strategies: {}, default_auth_strategy: 'noauth' } if @auth_config.nil?
157
+
158
+ # Strict mode: Detect strategy name collisions
159
+ if @auth_config[:auth_strategies].key?(name)
160
+ raise ArgumentError, "Authentication strategy '#{name}' is already registered"
161
+ end
162
+
163
+ @auth_config[:auth_strategies][name] = strategy
164
+ end
165
+ end
166
+ end
167
+ end
@@ -19,7 +19,7 @@ class Otto
19
19
  def call(env)
20
20
  return @app.call(env) unless @config.csrf_enabled?
21
21
 
22
- request = Rack::Request.new(env)
22
+ request = Otto::Request.new(env)
23
23
 
24
24
  # Skip CSRF protection for safe methods
25
25
  if safe_method?(request.request_method)
@@ -32,7 +32,7 @@ class Otto
32
32
  def call(env)
33
33
  return @app.call(env) unless @config.input_validation
34
34
 
35
- request = Rack::Request.new(env)
35
+ request = Otto::Request.new(env)
36
36
 
37
37
  begin
38
38
  # Validate request size
data/lib/otto/security.rb CHANGED
@@ -2,6 +2,7 @@
2
2
  #
3
3
  # frozen_string_literal: true
4
4
 
5
+ require_relative 'security/core'
5
6
  require_relative 'security/authentication/strategy_result'
6
7
  require_relative 'security/authorization_error'
7
8
  require_relative 'security/config'
data/lib/otto/version.rb CHANGED
@@ -3,5 +3,5 @@
3
3
  # frozen_string_literal: true
4
4
 
5
5
  class Otto
6
- VERSION = '2.0.0.pre9'
6
+ VERSION = '2.0.0.pre10'
7
7
  end