otto 1.5.0 → 1.6.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 +4 -4
- data/.github/workflows/ci.yml +43 -4
- data/.rubocop.yml +1 -1
- data/Gemfile +12 -3
- data/Gemfile.lock +51 -8
- data/bin/rspec +16 -0
- data/examples/mcp_demo/app.rb +56 -0
- data/examples/mcp_demo/config.ru +68 -0
- data/examples/mcp_demo/routes +9 -0
- data/lib/concurrent_cache_store.rb +68 -0
- data/lib/otto/helpers/validation.rb +83 -0
- data/lib/otto/mcp/auth/token.rb +76 -0
- data/lib/otto/mcp/protocol.rb +167 -0
- data/lib/otto/mcp/rate_limiting.rb +150 -0
- data/lib/otto/mcp/registry.rb +95 -0
- data/lib/otto/mcp/route_parser.rb +82 -0
- data/lib/otto/mcp/server.rb +196 -0
- data/lib/otto/mcp/validation.rb +119 -0
- data/lib/otto/route_definition.rb +15 -15
- data/lib/otto/route_handlers.rb +126 -97
- data/lib/otto/security/config.rb +3 -1
- data/lib/otto/security/rate_limiting.rb +111 -0
- data/lib/otto/security/validator.rb +35 -74
- data/lib/otto/version.rb +1 -1
- data/lib/otto.rb +127 -1
- data/otto.gemspec +11 -6
- metadata +67 -19
@@ -2,25 +2,21 @@
|
|
2
2
|
|
3
3
|
require 'json'
|
4
4
|
require 'cgi'
|
5
|
+
require 'loofah'
|
6
|
+
require 'facets/file'
|
7
|
+
|
8
|
+
require_relative '../helpers/validation'
|
5
9
|
|
6
10
|
class Otto
|
7
11
|
module Security
|
8
12
|
# ValidationMiddleware provides input validation and sanitization for web requests
|
13
|
+
# Uses Loofah for HTML/XSS sanitization and Facets for filename sanitization
|
9
14
|
class ValidationMiddleware
|
10
15
|
# Character validation patterns
|
11
16
|
INVALID_CHARACTERS = /[\x00-\x1f\x7f-\xff]/n
|
12
17
|
NULL_BYTE = /\0/
|
13
18
|
|
14
|
-
|
15
|
-
/<script[^>]*>/i, # Script tags
|
16
|
-
/javascript:/i, # JavaScript protocol
|
17
|
-
/data:.*base64/i, # Data URLs with base64
|
18
|
-
/on\w+\s*=/i, # Event handlers
|
19
|
-
/expression\s*\(/i, # CSS expressions
|
20
|
-
/url\s*\(/i, # CSS url() functions
|
21
|
-
NULL_BYTE, # Null bytes
|
22
|
-
INVALID_CHARACTERS, # Control characters and extended ASCII
|
23
|
-
].freeze
|
19
|
+
# HTML/XSS sanitization is handled by Loofah library for better security coverage
|
24
20
|
|
25
21
|
SQL_INJECTION_PATTERNS = [
|
26
22
|
/('|(\\')|(;)|(\\)|(--)|(%27)|(%3B)|(%3D))/i,
|
@@ -95,7 +91,7 @@ class Otto
|
|
95
91
|
end
|
96
92
|
|
97
93
|
def validate_param_structure(params, depth = 0)
|
98
|
-
if depth
|
94
|
+
if depth >= @config.max_param_depth
|
99
95
|
raise Otto::Security::ValidationError, "Parameter depth exceeds maximum (#{@config.max_param_depth})"
|
100
96
|
end
|
101
97
|
|
@@ -150,32 +146,44 @@ class Otto
|
|
150
146
|
def sanitize_value(value)
|
151
147
|
return value unless value.is_a?(String)
|
152
148
|
|
153
|
-
# Check for
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
149
|
+
# Check for extremely long values first
|
150
|
+
if value.length > 10_000
|
151
|
+
raise Otto::Security::ValidationError, "Parameter value too long (#{value.length} characters)"
|
152
|
+
end
|
153
|
+
|
154
|
+
# Start with the original value
|
155
|
+
original = value.dup
|
156
|
+
|
157
|
+
# Check for null bytes first (these should be rejected, not sanitized)
|
158
|
+
if original.match?(NULL_BYTE)
|
159
|
+
raise Otto::Security::ValidationError, 'Dangerous content detected in parameter'
|
160
|
+
end
|
161
|
+
|
162
|
+
# Check for script injection first (these should always be rejected)
|
163
|
+
if looks_like_script_injection?(original)
|
164
|
+
raise Otto::Security::ValidationError, 'Dangerous content detected in parameter'
|
158
165
|
end
|
159
166
|
|
167
|
+
# Use Loofah to sanitize HTML/XSS content for less dangerous HTML
|
168
|
+
# Loofah.fragment removes dangerous HTML but preserves safe content
|
169
|
+
sanitized = Loofah.fragment(original).scrub!(:whitewash).to_s
|
170
|
+
|
171
|
+
# Remove control characters (sanitize, don't block)
|
172
|
+
sanitized = sanitized.gsub(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/, '')
|
173
|
+
|
160
174
|
# Check for SQL injection patterns
|
161
175
|
SQL_INJECTION_PATTERNS.each do |pattern|
|
162
|
-
if
|
176
|
+
if sanitized.match?(pattern)
|
163
177
|
raise Otto::Security::ValidationError, 'Potential SQL injection detected'
|
164
178
|
end
|
165
179
|
end
|
166
180
|
|
167
|
-
|
168
|
-
|
169
|
-
raise Otto::Security::ValidationError, "Parameter value too long (#{value.length} characters)"
|
170
|
-
end
|
181
|
+
sanitized
|
182
|
+
end
|
171
183
|
|
172
|
-
|
173
|
-
sanitized = value.gsub(/\0/, '').gsub(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/, '')
|
184
|
+
include ValidationHelpers
|
174
185
|
|
175
|
-
|
176
|
-
sanitized = sanitized.gsub(/<!--.*?-->/m, '') # Remove HTML comments
|
177
|
-
sanitized.gsub(/<!\[CDATA\[.*?\]\]>/m, '') # Remove CDATA sections
|
178
|
-
end
|
186
|
+
private
|
179
187
|
|
180
188
|
def validate_headers(request)
|
181
189
|
# Check for dangerous headers
|
@@ -248,52 +256,5 @@ class Otto
|
|
248
256
|
}.to_json
|
249
257
|
end
|
250
258
|
end
|
251
|
-
|
252
|
-
module ValidationHelpers
|
253
|
-
def validate_input(input, max_length: 1000, allow_html: false)
|
254
|
-
return input if input.nil? || input.empty?
|
255
|
-
|
256
|
-
input_str = input.to_s
|
257
|
-
|
258
|
-
# Check length
|
259
|
-
if input_str.length > max_length
|
260
|
-
raise Otto::Security::ValidationError, "Input too long (#{input_str.length} > #{max_length})"
|
261
|
-
end
|
262
|
-
|
263
|
-
# Check for dangerous patterns unless HTML is allowed
|
264
|
-
unless allow_html
|
265
|
-
ValidationMiddleware::DANGEROUS_PATTERNS.each do |pattern|
|
266
|
-
if input_str.match?(pattern)
|
267
|
-
raise Otto::Security::ValidationError, 'Dangerous content detected'
|
268
|
-
end
|
269
|
-
end
|
270
|
-
end
|
271
|
-
|
272
|
-
# Always check for SQL injection
|
273
|
-
ValidationMiddleware::SQL_INJECTION_PATTERNS.each do |pattern|
|
274
|
-
if input_str.match?(pattern)
|
275
|
-
raise Otto::Security::ValidationError, 'Potential SQL injection detected'
|
276
|
-
end
|
277
|
-
end
|
278
|
-
|
279
|
-
input_str
|
280
|
-
end
|
281
|
-
|
282
|
-
def sanitize_filename(filename)
|
283
|
-
return nil if filename.nil? || filename.empty?
|
284
|
-
|
285
|
-
# Remove path components and dangerous characters
|
286
|
-
clean_name = File.basename(filename.to_s)
|
287
|
-
clean_name = clean_name.gsub(/[^\w\-_\.]/, '_')
|
288
|
-
clean_name = clean_name.gsub(/_{2,}/, '_')
|
289
|
-
clean_name = clean_name.gsub(/^_+|_+$/, '')
|
290
|
-
|
291
|
-
# Ensure it's not empty and has reasonable length
|
292
|
-
clean_name = 'file' if clean_name.empty?
|
293
|
-
clean_name = clean_name[0..100] if clean_name.length > 100
|
294
|
-
|
295
|
-
clean_name
|
296
|
-
end
|
297
|
-
end
|
298
259
|
end
|
299
260
|
end
|
data/lib/otto/version.rb
CHANGED
data/lib/otto.rb
CHANGED
@@ -20,6 +20,8 @@ require_relative 'otto/security/config'
|
|
20
20
|
require_relative 'otto/security/csrf'
|
21
21
|
require_relative 'otto/security/validator'
|
22
22
|
require_relative 'otto/security/authentication'
|
23
|
+
require_relative 'otto/security/rate_limiting'
|
24
|
+
require_relative 'otto/mcp/server'
|
23
25
|
|
24
26
|
# Otto is a simple Rack router that allows you to define routes in a file
|
25
27
|
# with built-in security features including CSRF protection, input validation,
|
@@ -60,7 +62,7 @@ class Otto
|
|
60
62
|
@global_config
|
61
63
|
end
|
62
64
|
|
63
|
-
attr_reader :routes, :routes_literal, :routes_static, :route_definitions, :option, :static_route, :security_config, :locale_config, :auth_config, :route_handler_factory
|
65
|
+
attr_reader :routes, :routes_literal, :routes_static, :route_definitions, :option, :static_route, :security_config, :locale_config, :auth_config, :route_handler_factory, :mcp_server
|
64
66
|
attr_accessor :not_found, :server_error, :middleware_stack
|
65
67
|
|
66
68
|
def initialize(path = nil, opts = {})
|
@@ -85,6 +87,9 @@ class Otto
|
|
85
87
|
# Configure authentication based on options
|
86
88
|
configure_authentication(opts)
|
87
89
|
|
90
|
+
# Initialize MCP server
|
91
|
+
configure_mcp(opts)
|
92
|
+
|
88
93
|
Otto.logger.debug "new Otto: #{opts}" if Otto.debug
|
89
94
|
load(path) unless path.nil?
|
90
95
|
super()
|
@@ -101,6 +106,16 @@ class Otto
|
|
101
106
|
# This preserves parameters in the definition part
|
102
107
|
parts = entry.split(/\s+/, 3)
|
103
108
|
verb, path, definition = parts[0], parts[1], parts[2]
|
109
|
+
|
110
|
+
# Check for MCP routes
|
111
|
+
if Otto::MCP::RouteParser.is_mcp_route?(definition)
|
112
|
+
handle_mcp_route(verb, path, definition) if @mcp_server
|
113
|
+
next
|
114
|
+
elsif Otto::MCP::RouteParser.is_tool_route?(definition)
|
115
|
+
handle_tool_route(verb, path, definition) if @mcp_server
|
116
|
+
next
|
117
|
+
end
|
118
|
+
|
104
119
|
route = Otto::Route.new verb, path, definition
|
105
120
|
route.otto = self
|
106
121
|
path_clean = path.gsub(%r{/$}, '')
|
@@ -343,6 +358,52 @@ class Otto
|
|
343
358
|
use Otto::Security::ValidationMiddleware
|
344
359
|
end
|
345
360
|
|
361
|
+
# Enable rate limiting to protect against abuse and DDoS attacks.
|
362
|
+
# This will automatically add rate limiting rules based on client IP.
|
363
|
+
#
|
364
|
+
# @param options [Hash] Rate limiting configuration options
|
365
|
+
# @option options [Integer] :requests_per_minute Maximum requests per minute per IP (default: 100)
|
366
|
+
# @option options [Hash] :custom_rules Custom rate limiting rules
|
367
|
+
# @example
|
368
|
+
# otto.enable_rate_limiting!(requests_per_minute: 50)
|
369
|
+
def enable_rate_limiting!(options = {})
|
370
|
+
return if middleware_enabled?(Otto::Security::RateLimitMiddleware)
|
371
|
+
|
372
|
+
configure_rate_limiting(options)
|
373
|
+
use Otto::Security::RateLimitMiddleware
|
374
|
+
end
|
375
|
+
|
376
|
+
# Configure rate limiting settings.
|
377
|
+
#
|
378
|
+
# @param config [Hash] Rate limiting configuration
|
379
|
+
# @option config [Integer] :requests_per_minute Maximum requests per minute per IP
|
380
|
+
# @option config [Hash] :custom_rules Hash of custom rate limiting rules
|
381
|
+
# @option config [Object] :cache_store Custom cache store for rate limiting
|
382
|
+
# @example
|
383
|
+
# otto.configure_rate_limiting({
|
384
|
+
# requests_per_minute: 50,
|
385
|
+
# custom_rules: {
|
386
|
+
# 'api_calls' => { limit: 30, period: 60, condition: ->(req) { req.path.start_with?('/api') }}
|
387
|
+
# }
|
388
|
+
# })
|
389
|
+
def configure_rate_limiting(config)
|
390
|
+
@security_config.rate_limiting_config.merge!(config)
|
391
|
+
end
|
392
|
+
|
393
|
+
# Add a custom rate limiting rule.
|
394
|
+
#
|
395
|
+
# @param name [String, Symbol] Rule name
|
396
|
+
# @param options [Hash] Rule configuration
|
397
|
+
# @option options [Integer] :limit Maximum requests
|
398
|
+
# @option options [Integer] :period Time period in seconds (default: 60)
|
399
|
+
# @option options [Proc] :condition Optional condition proc that receives request
|
400
|
+
# @example
|
401
|
+
# otto.add_rate_limit_rule('uploads', limit: 5, period: 300, condition: ->(req) { req.post? && req.path.include?('upload') })
|
402
|
+
def add_rate_limit_rule(name, options)
|
403
|
+
@security_config.rate_limiting_config[:custom_rules] ||= {}
|
404
|
+
@security_config.rate_limiting_config[:custom_rules][name.to_s] = options
|
405
|
+
end
|
406
|
+
|
346
407
|
# Add a trusted proxy server for accurate client IP detection.
|
347
408
|
# Only requests from trusted proxies will have their forwarded headers honored.
|
348
409
|
#
|
@@ -466,6 +527,29 @@ class Otto
|
|
466
527
|
enable_authentication!
|
467
528
|
end
|
468
529
|
|
530
|
+
# Enable MCP (Model Context Protocol) server support
|
531
|
+
#
|
532
|
+
# @param options [Hash] MCP configuration options
|
533
|
+
# @option options [Boolean] :http Enable HTTP endpoint (default: true)
|
534
|
+
# @option options [Boolean] :stdio Enable STDIO communication (default: false)
|
535
|
+
# @option options [String] :endpoint HTTP endpoint path (default: '/_mcp')
|
536
|
+
# @example
|
537
|
+
# otto.enable_mcp!(http: true, endpoint: '/api/mcp')
|
538
|
+
def enable_mcp!(options = {})
|
539
|
+
unless @mcp_server
|
540
|
+
@mcp_server = Otto::MCP::Server.new(self)
|
541
|
+
end
|
542
|
+
|
543
|
+
@mcp_server.enable!(options)
|
544
|
+
Otto.logger.info "[MCP] Enabled MCP server" if Otto.debug
|
545
|
+
end
|
546
|
+
|
547
|
+
# Check if MCP is enabled
|
548
|
+
# @return [Boolean]
|
549
|
+
def mcp_enabled?
|
550
|
+
@mcp_server&.enabled?
|
551
|
+
end
|
552
|
+
|
469
553
|
private
|
470
554
|
|
471
555
|
def configure_locale(opts)
|
@@ -506,6 +590,12 @@ class Otto
|
|
506
590
|
# Enable request validation if requested
|
507
591
|
enable_request_validation! if opts[:request_validation]
|
508
592
|
|
593
|
+
# Enable rate limiting if requested
|
594
|
+
if opts[:rate_limiting]
|
595
|
+
rate_limiting_opts = opts[:rate_limiting].is_a?(Hash) ? opts[:rate_limiting] : {}
|
596
|
+
enable_rate_limiting!(rate_limiting_opts)
|
597
|
+
end
|
598
|
+
|
509
599
|
# Add trusted proxies if provided
|
510
600
|
if opts[:trusted_proxies]
|
511
601
|
Array(opts[:trusted_proxies]).each { |proxy| add_trusted_proxy(proxy) }
|
@@ -534,6 +624,42 @@ class Otto
|
|
534
624
|
end
|
535
625
|
end
|
536
626
|
|
627
|
+
def configure_mcp(opts)
|
628
|
+
@mcp_server = nil
|
629
|
+
|
630
|
+
# Enable MCP if requested in options
|
631
|
+
if opts[:mcp_enabled] || opts[:mcp_http] || opts[:mcp_stdio]
|
632
|
+
@mcp_server = Otto::MCP::Server.new(self)
|
633
|
+
|
634
|
+
mcp_options = {}
|
635
|
+
mcp_options[:http_endpoint] = opts[:mcp_endpoint] if opts[:mcp_endpoint]
|
636
|
+
|
637
|
+
if opts[:mcp_http] != false # Default to true unless explicitly disabled
|
638
|
+
@mcp_server.enable!(mcp_options)
|
639
|
+
end
|
640
|
+
end
|
641
|
+
end
|
642
|
+
|
643
|
+
def handle_mcp_route(verb, path, definition)
|
644
|
+
begin
|
645
|
+
route_info = Otto::MCP::RouteParser.parse_mcp_route(verb, path, definition)
|
646
|
+
@mcp_server.register_mcp_route(route_info)
|
647
|
+
Otto.logger.debug "[MCP] Registered resource route: #{definition}" if Otto.debug
|
648
|
+
rescue => e
|
649
|
+
Otto.logger.error "[MCP] Failed to parse MCP route: #{definition} - #{e.message}"
|
650
|
+
end
|
651
|
+
end
|
652
|
+
|
653
|
+
def handle_tool_route(verb, path, definition)
|
654
|
+
begin
|
655
|
+
route_info = Otto::MCP::RouteParser.parse_tool_route(verb, path, definition)
|
656
|
+
@mcp_server.register_mcp_route(route_info)
|
657
|
+
Otto.logger.debug "[MCP] Registered tool route: #{definition}" if Otto.debug
|
658
|
+
rescue => e
|
659
|
+
Otto.logger.error "[MCP] Failed to parse TOOL route: #{definition} - #{e.message}"
|
660
|
+
end
|
661
|
+
end
|
662
|
+
|
537
663
|
def handle_error(error, env)
|
538
664
|
# Log error details internally but don't expose them
|
539
665
|
error_id = SecureRandom.hex(8)
|
data/otto.gemspec
CHANGED
@@ -10,18 +10,23 @@ Gem::Specification.new do |spec|
|
|
10
10
|
spec.email = 'gems@solutious.com'
|
11
11
|
spec.authors = ['Delano Mandelbaum']
|
12
12
|
spec.license = 'MIT'
|
13
|
-
spec.files =
|
14
|
-
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
15
|
-
end
|
13
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
16
14
|
spec.homepage = 'https://github.com/delano/otto'
|
17
15
|
spec.require_paths = ['lib']
|
18
16
|
|
19
17
|
spec.required_ruby_version = ['>= 3.2', '< 4.0']
|
20
18
|
|
21
|
-
|
22
|
-
spec.add_dependency 'rexml', '>= 3.3.6'
|
23
|
-
spec.add_dependency 'ostruct'
|
19
|
+
spec.add_dependency 'ostruct', '~> 0.6.3'
|
24
20
|
spec.add_dependency 'rack', '~> 3.1', '< 4.0'
|
25
21
|
spec.add_dependency 'rack-parser', '~> 0.7'
|
22
|
+
spec.add_dependency 'rexml', '~> 3.3', '>= 3.3.6'
|
23
|
+
|
24
|
+
# Security dependencies
|
25
|
+
spec.add_dependency 'facets', '~> 3.1'
|
26
|
+
spec.add_dependency 'loofah', '~> 2.20'
|
27
|
+
|
28
|
+
# Optional MCP dependencies
|
29
|
+
# spec.add_dependency 'json_schemer', '~> 2.0'
|
30
|
+
# spec.add_dependency 'rack-attack', '~> 6.7'
|
26
31
|
spec.metadata['rubygems_mfa_required'] = 'true'
|
27
32
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: otto
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.6.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Delano Mandelbaum
|
@@ -9,34 +9,20 @@ bindir: bin
|
|
9
9
|
cert_chain: []
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
11
11
|
dependencies:
|
12
|
-
- !ruby/object:Gem::Dependency
|
13
|
-
name: rexml
|
14
|
-
requirement: !ruby/object:Gem::Requirement
|
15
|
-
requirements:
|
16
|
-
- - ">="
|
17
|
-
- !ruby/object:Gem::Version
|
18
|
-
version: 3.3.6
|
19
|
-
type: :runtime
|
20
|
-
prerelease: false
|
21
|
-
version_requirements: !ruby/object:Gem::Requirement
|
22
|
-
requirements:
|
23
|
-
- - ">="
|
24
|
-
- !ruby/object:Gem::Version
|
25
|
-
version: 3.3.6
|
26
12
|
- !ruby/object:Gem::Dependency
|
27
13
|
name: ostruct
|
28
14
|
requirement: !ruby/object:Gem::Requirement
|
29
15
|
requirements:
|
30
|
-
- - "
|
16
|
+
- - "~>"
|
31
17
|
- !ruby/object:Gem::Version
|
32
|
-
version:
|
18
|
+
version: 0.6.3
|
33
19
|
type: :runtime
|
34
20
|
prerelease: false
|
35
21
|
version_requirements: !ruby/object:Gem::Requirement
|
36
22
|
requirements:
|
37
|
-
- - "
|
23
|
+
- - "~>"
|
38
24
|
- !ruby/object:Gem::Version
|
39
|
-
version:
|
25
|
+
version: 0.6.3
|
40
26
|
- !ruby/object:Gem::Dependency
|
41
27
|
name: rack
|
42
28
|
requirement: !ruby/object:Gem::Requirement
|
@@ -71,6 +57,54 @@ dependencies:
|
|
71
57
|
- - "~>"
|
72
58
|
- !ruby/object:Gem::Version
|
73
59
|
version: '0.7'
|
60
|
+
- !ruby/object:Gem::Dependency
|
61
|
+
name: rexml
|
62
|
+
requirement: !ruby/object:Gem::Requirement
|
63
|
+
requirements:
|
64
|
+
- - "~>"
|
65
|
+
- !ruby/object:Gem::Version
|
66
|
+
version: '3.3'
|
67
|
+
- - ">="
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: 3.3.6
|
70
|
+
type: :runtime
|
71
|
+
prerelease: false
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
requirements:
|
74
|
+
- - "~>"
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
version: '3.3'
|
77
|
+
- - ">="
|
78
|
+
- !ruby/object:Gem::Version
|
79
|
+
version: 3.3.6
|
80
|
+
- !ruby/object:Gem::Dependency
|
81
|
+
name: facets
|
82
|
+
requirement: !ruby/object:Gem::Requirement
|
83
|
+
requirements:
|
84
|
+
- - "~>"
|
85
|
+
- !ruby/object:Gem::Version
|
86
|
+
version: '3.1'
|
87
|
+
type: :runtime
|
88
|
+
prerelease: false
|
89
|
+
version_requirements: !ruby/object:Gem::Requirement
|
90
|
+
requirements:
|
91
|
+
- - "~>"
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: '3.1'
|
94
|
+
- !ruby/object:Gem::Dependency
|
95
|
+
name: loofah
|
96
|
+
requirement: !ruby/object:Gem::Requirement
|
97
|
+
requirements:
|
98
|
+
- - "~>"
|
99
|
+
- !ruby/object:Gem::Version
|
100
|
+
version: '2.20'
|
101
|
+
type: :runtime
|
102
|
+
prerelease: false
|
103
|
+
version_requirements: !ruby/object:Gem::Requirement
|
104
|
+
requirements:
|
105
|
+
- - "~>"
|
106
|
+
- !ruby/object:Gem::Version
|
107
|
+
version: '2.20'
|
74
108
|
description: 'Otto: Auto-define your rack-apps in plaintext.'
|
75
109
|
email: gems@solutious.com
|
76
110
|
executables: []
|
@@ -88,6 +122,7 @@ files:
|
|
88
122
|
- Gemfile.lock
|
89
123
|
- LICENSE.txt
|
90
124
|
- README.md
|
125
|
+
- bin/rspec
|
91
126
|
- docs/.gitignore
|
92
127
|
- examples/basic/app.rb
|
93
128
|
- examples/basic/config.ru
|
@@ -98,14 +133,26 @@ files:
|
|
98
133
|
- examples/helpers_demo/app.rb
|
99
134
|
- examples/helpers_demo/config.ru
|
100
135
|
- examples/helpers_demo/routes
|
136
|
+
- examples/mcp_demo/app.rb
|
137
|
+
- examples/mcp_demo/config.ru
|
138
|
+
- examples/mcp_demo/routes
|
101
139
|
- examples/security_features/app.rb
|
102
140
|
- examples/security_features/config.ru
|
103
141
|
- examples/security_features/routes
|
142
|
+
- lib/concurrent_cache_store.rb
|
104
143
|
- lib/otto.rb
|
105
144
|
- lib/otto/design_system.rb
|
106
145
|
- lib/otto/helpers/base.rb
|
107
146
|
- lib/otto/helpers/request.rb
|
108
147
|
- lib/otto/helpers/response.rb
|
148
|
+
- lib/otto/helpers/validation.rb
|
149
|
+
- lib/otto/mcp/auth/token.rb
|
150
|
+
- lib/otto/mcp/protocol.rb
|
151
|
+
- lib/otto/mcp/rate_limiting.rb
|
152
|
+
- lib/otto/mcp/registry.rb
|
153
|
+
- lib/otto/mcp/route_parser.rb
|
154
|
+
- lib/otto/mcp/server.rb
|
155
|
+
- lib/otto/mcp/validation.rb
|
109
156
|
- lib/otto/response_handlers.rb
|
110
157
|
- lib/otto/route.rb
|
111
158
|
- lib/otto/route_definition.rb
|
@@ -113,6 +160,7 @@ files:
|
|
113
160
|
- lib/otto/security/authentication.rb
|
114
161
|
- lib/otto/security/config.rb
|
115
162
|
- lib/otto/security/csrf.rb
|
163
|
+
- lib/otto/security/rate_limiting.rb
|
116
164
|
- lib/otto/security/validator.rb
|
117
165
|
- lib/otto/static.rb
|
118
166
|
- lib/otto/version.rb
|