otto 1.2.0 → 1.3.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.
@@ -5,31 +5,32 @@ require 'cgi'
5
5
 
6
6
  class Otto
7
7
  module Security
8
+ # ValidationMiddleware provides input validation and sanitization for web requests
8
9
  class ValidationMiddleware
9
10
  # Character validation patterns
10
- INVALID_CHARACTERS = /[\x00-\x1f\x7f-\xff]/n.freeze
11
- NULL_BYTE = /\0/.freeze
11
+ INVALID_CHARACTERS = /[\x00-\x1f\x7f-\xff]/n
12
+ NULL_BYTE = /\0/
12
13
 
13
14
  DANGEROUS_PATTERNS = [
14
15
  /<script[^>]*>/i, # Script tags
15
16
  /javascript:/i, # JavaScript protocol
16
17
  /data:.*base64/i, # Data URLs with base64
17
- /on\w+\s*=/i, # Event handlers
18
+ /on\w+\s*=/i, # Event handlers
18
19
  /expression\s*\(/i, # CSS expressions
19
- /url\s*\(/i, # CSS url() functions
20
- NULL_BYTE, # Null bytes
21
- INVALID_CHARACTERS # Control characters and extended ASCII
20
+ /url\s*\(/i, # CSS url() functions
21
+ NULL_BYTE, # Null bytes
22
+ INVALID_CHARACTERS, # Control characters and extended ASCII
22
23
  ].freeze
23
24
 
24
25
  SQL_INJECTION_PATTERNS = [
25
26
  /('|(\\')|(;)|(\\)|(--)|(%27)|(%3B)|(%3D))/i,
26
27
  /(union|select|insert|update|delete|drop|create|alter|exec|execute)/i,
27
28
  /(or|and)\s+\w+\s*=\s*\w+/i,
28
- /\d+\s*(=|>|<|>=|<=|<>|!=)\s*\d+/i
29
+ /\d+\s*(=|>|<|>=|<=|<>|!=)\s*\d+/i,
29
30
  ].freeze
30
31
 
31
32
  def initialize(app, config = nil)
32
- @app = app
33
+ @app = app
33
34
  @config = config || Otto::Security::Config.new
34
35
  end
35
36
 
@@ -46,17 +47,21 @@ class Otto
46
47
  validate_content_type(request)
47
48
 
48
49
  # Validate and sanitize parameters
49
- validate_parameters(request) if request.params
50
+ begin
51
+ validate_parameters(request) if request.params
52
+ rescue Rack::QueryParser::QueryLimitError => ex
53
+ # Handle Rack's built-in query parsing limits
54
+ raise Otto::Security::ValidationError, "Parameter structure too complex: #{ex.message}"
55
+ end
50
56
 
51
57
  # Validate headers
52
58
  validate_headers(request)
53
59
 
54
60
  @app.call(env)
55
-
56
- rescue Otto::Security::ValidationError => e
57
- return validation_error_response(e.message)
58
- rescue Otto::Security::RequestTooLargeError => e
59
- return request_too_large_response(e.message)
61
+ rescue Otto::Security::ValidationError => ex
62
+ validation_error_response(ex.message)
63
+ rescue Otto::Security::RequestTooLargeError => ex
64
+ request_too_large_response(ex.message)
60
65
  end
61
66
  end
62
67
 
@@ -76,7 +81,7 @@ class Otto
76
81
  'application/x-shockwave-flash',
77
82
  'application/x-silverlight-app',
78
83
  'text/vbscript',
79
- 'application/vbscript'
84
+ 'application/vbscript',
80
85
  ]
81
86
 
82
87
  if dangerous_types.any? { |type| content_type.downcase.include?(type) }
@@ -148,14 +153,14 @@ class Otto
148
153
  # Check for dangerous patterns
149
154
  DANGEROUS_PATTERNS.each do |pattern|
150
155
  if value.match?(pattern)
151
- raise Otto::Security::ValidationError, "Dangerous content detected in parameter"
156
+ raise Otto::Security::ValidationError, 'Dangerous content detected in parameter'
152
157
  end
153
158
  end
154
159
 
155
160
  # Check for SQL injection patterns
156
161
  SQL_INJECTION_PATTERNS.each do |pattern|
157
162
  if value.match?(pattern)
158
- raise Otto::Security::ValidationError, "Potential SQL injection detected"
163
+ raise Otto::Security::ValidationError, 'Potential SQL injection detected'
159
164
  end
160
165
  end
161
166
 
@@ -169,9 +174,7 @@ class Otto
169
174
 
170
175
  # Additional sanitization for common attack vectors
171
176
  sanitized = sanitized.gsub(/<!--.*?-->/m, '') # Remove HTML comments
172
- sanitized = sanitized.gsub(/<!\[CDATA\[.*?\]\]>/m, '') # Remove CDATA sections
173
-
174
- sanitized
177
+ sanitized.gsub(/<!\[CDATA\[.*?\]\]>/m, '') # Remove CDATA sections
175
178
  end
176
179
 
177
180
  def validate_headers(request)
@@ -197,13 +200,13 @@ class Otto
197
200
  # Validate User-Agent length
198
201
  user_agent = request.env['HTTP_USER_AGENT']
199
202
  if user_agent && user_agent.length > 1000
200
- raise Otto::Security::ValidationError, "User-Agent header too long"
203
+ raise Otto::Security::ValidationError, 'User-Agent header too long'
201
204
  end
202
205
 
203
206
  # Validate Referer header
204
207
  referer = request.env['HTTP_REFERER']
205
208
  if referer && referer.length > 2000
206
- raise Otto::Security::ValidationError, "Referer header too long"
209
+ raise Otto::Security::ValidationError, 'Referer header too long'
207
210
  end
208
211
  end
209
212
 
@@ -212,9 +215,9 @@ class Otto
212
215
  400,
213
216
  {
214
217
  'content-type' => 'application/json',
215
- 'content-length' => validation_error_body(message).bytesize.to_s
218
+ 'content-length' => validation_error_body(message).bytesize.to_s,
216
219
  },
217
- [validation_error_body(message)]
220
+ [validation_error_body(message)],
218
221
  ]
219
222
  end
220
223
 
@@ -223,9 +226,9 @@ class Otto
223
226
  413,
224
227
  {
225
228
  'content-type' => 'application/json',
226
- 'content-length' => request_too_large_body(message).bytesize.to_s
229
+ 'content-length' => request_too_large_body(message).bytesize.to_s,
227
230
  },
228
- [request_too_large_body(message)]
231
+ [request_too_large_body(message)],
229
232
  ]
230
233
  end
231
234
 
@@ -233,7 +236,7 @@ class Otto
233
236
  require 'json'
234
237
  {
235
238
  error: 'Validation failed',
236
- message: message
239
+ message: message,
237
240
  }.to_json
238
241
  end
239
242
 
@@ -241,7 +244,7 @@ class Otto
241
244
  require 'json'
242
245
  {
243
246
  error: 'Request too large',
244
- message: message
247
+ message: message,
245
248
  }.to_json
246
249
  end
247
250
  end
@@ -261,7 +264,7 @@ class Otto
261
264
  unless allow_html
262
265
  ValidationMiddleware::DANGEROUS_PATTERNS.each do |pattern|
263
266
  if input_str.match?(pattern)
264
- raise Otto::Security::ValidationError, "Dangerous content detected"
267
+ raise Otto::Security::ValidationError, 'Dangerous content detected'
265
268
  end
266
269
  end
267
270
  end
@@ -269,7 +272,7 @@ class Otto
269
272
  # Always check for SQL injection
270
273
  ValidationMiddleware::SQL_INJECTION_PATTERNS.each do |pattern|
271
274
  if input_str.match?(pattern)
272
- raise Otto::Security::ValidationError, "Potential SQL injection detected"
275
+ raise Otto::Security::ValidationError, 'Potential SQL injection detected'
273
276
  end
274
277
  end
275
278
 
data/lib/otto/static.rb CHANGED
@@ -17,7 +17,7 @@ class Otto
17
17
  'x-frame-options' => 'DENY',
18
18
  'x-content-type-options' => 'nosniff',
19
19
  'x-xss-protection' => '1; mode=block',
20
- 'referrer-policy' => 'strict-origin-when-cross-origin'
20
+ 'referrer-policy' => 'strict-origin-when-cross-origin',
21
21
  }
22
22
  end
23
23
 
data/lib/otto/version.rb CHANGED
@@ -1,29 +1,5 @@
1
1
  # lib/otto/version.rb
2
2
 
3
3
  class Otto
4
- # Otto::VERSION
5
- #
6
- module VERSION
7
- def self.to_a
8
- load_config
9
- version = [@version[:MAJOR], @version[:MINOR], @version[:PATCH]]
10
- version << @version[:PRE] unless @version.fetch(:PRE, nil).to_s.empty?
11
- version
12
- end
13
-
14
- def self.to_s
15
- to_a.join('.')
16
- end
17
-
18
- def self.inspect
19
- to_s
20
- end
21
-
22
- def self.load_config
23
- return if @version
24
-
25
- require 'yaml'
26
- @version = YAML.load_file(File.join(__dir__, '..', '..', 'VERSION.yml'))
27
- end
28
- end
4
+ VERSION = '1.3.0'.freeze
29
5
  end
data/lib/otto.rb CHANGED
@@ -1,10 +1,11 @@
1
+ require 'json'
1
2
  require 'logger'
2
3
  require 'securerandom'
4
+ require 'uri'
3
5
 
4
6
  require 'rack/request'
5
7
  require 'rack/response'
6
8
  require 'rack/utils'
7
- require 'addressable/uri'
8
9
 
9
10
  require_relative 'otto/route'
10
11
  require_relative 'otto/static'
@@ -39,23 +40,23 @@ require_relative 'otto/security/validator'
39
40
  class Otto
40
41
  LIB_HOME = __dir__ unless defined?(Otto::LIB_HOME)
41
42
 
42
- @debug = ENV['OTTO_DEBUG'] == 'true'
43
+ @debug = ENV['OTTO_DEBUG'] == 'true'
43
44
  @logger = Logger.new($stdout, Logger::INFO)
44
45
 
45
46
  attr_reader :routes, :routes_literal, :routes_static, :route_definitions, :option, :static_route, :security_config
46
47
  attr_accessor :not_found, :server_error, :middleware_stack
47
48
 
48
49
  def initialize(path = nil, opts = {})
49
- @routes_static = { GET: {} }
50
- @routes = { GET: [] }
51
- @routes_literal = { GET: {} }
50
+ @routes_static = { GET: {} }
51
+ @routes = { GET: [] }
52
+ @routes_literal = { GET: {} }
52
53
  @route_definitions = {}
53
- @option = {
54
+ @option = {
54
55
  public: nil,
55
- locale: 'en'
56
+ locale: 'en',
56
57
  }.merge(opts)
57
- @security_config = Otto::Security::Config.new
58
- @middleware_stack = []
58
+ @security_config = Otto::Security::Config.new
59
+ @middleware_stack = []
59
60
 
60
61
  # Configure security based on options
61
62
  configure_security(opts)
@@ -72,15 +73,15 @@ class Otto
72
73
 
73
74
  raw = File.readlines(path).select { |line| line =~ /^\w/ }.collect { |line| line.strip.split(/\s+/) }
74
75
  raw.each do |entry|
75
- verb, path, definition = *entry
76
- route = Otto::Route.new verb, path, definition
77
- route.otto = self
78
- path_clean = path.gsub(%r{/$}, '')
79
- @route_definitions[route.definition] = route
76
+ verb, path, definition = *entry
77
+ route = Otto::Route.new verb, path, definition
78
+ route.otto = self
79
+ path_clean = path.gsub(%r{/$}, '')
80
+ @route_definitions[route.definition] = route
80
81
  Otto.logger.debug "route: #{route.pattern}" if Otto.debug
81
- @routes[route.verb] ||= []
82
+ @routes[route.verb] ||= []
82
83
  @routes[route.verb] << route
83
- @routes_literal[route.verb] ||= {}
84
+ @routes_literal[route.verb] ||= {}
84
85
  @routes_literal[route.verb][path_clean] = route
85
86
  rescue StandardError
86
87
  Otto.logger.error "Bad route in #{path}: #{entry}"
@@ -97,7 +98,7 @@ class Otto
97
98
  return false unless File.directory?(public_dir)
98
99
 
99
100
  # Clean the requested path - remove null bytes and normalize
100
- clean_path = path.gsub("\0", "").strip
101
+ clean_path = path.delete("\0").strip
101
102
  return false if clean_path.empty?
102
103
 
103
104
  # Join and expand to get the full resolved path
@@ -111,13 +112,13 @@ class Otto
111
112
  File.readable?(requested_path) &&
112
113
  !File.directory?(requested_path) &&
113
114
  (File.owned?(requested_path) || File.grpowned?(requested_path))
114
- end
115
+ end
115
116
 
116
117
  def safe_dir?(path)
117
118
  return false if path.nil? || path.empty?
118
119
 
119
120
  # Clean and expand the path
120
- clean_path = path.gsub("\0", "").strip
121
+ clean_path = path.delete("\0").strip
121
122
  return false if clean_path.empty?
122
123
 
123
124
  expanded_path = File.expand_path(clean_path)
@@ -131,9 +132,9 @@ end
131
132
  def add_static_path(path)
132
133
  return unless safe_file?(path)
133
134
 
134
- base_path = File.split(path).first
135
+ base_path = File.split(path).first
135
136
  # Files in the root directory can refer to themselves
136
- base_path = path if base_path == '/'
137
+ base_path = path if base_path == '/'
137
138
  File.join(option[:public], base_path)
138
139
  Otto.logger.debug "new static route: #{base_path} (#{path})" if Otto.debug
139
140
  routes_static[:GET][base_path] = base_path
@@ -141,46 +142,46 @@ end
141
142
 
142
143
  def call(env)
143
144
  # Apply middleware stack
144
- app = lambda { |e| handle_request(e) }
145
- @middleware_stack.reverse.each do |middleware|
145
+ app = ->(e) { handle_request(e) }
146
+ @middleware_stack.reverse_each do |middleware|
146
147
  app = middleware.new(app, @security_config)
147
148
  end
148
149
 
149
150
  begin
150
151
  app.call(env)
151
- rescue StandardError => e
152
- handle_error(e, env)
152
+ rescue StandardError => ex
153
+ handle_error(ex, env)
153
154
  end
154
155
  end
155
156
 
156
157
  def handle_request(env)
157
- locale = determine_locale env
158
+ locale = determine_locale env
158
159
  env['rack.locale'] = locale
159
- @static_route ||= Rack::Files.new(option[:public]) if option[:public] && safe_dir?(option[:public])
160
- path_info = Rack::Utils.unescape(env['PATH_INFO'])
161
- path_info = '/' if path_info.to_s.empty?
160
+ @static_route ||= Rack::Files.new(option[:public]) if option[:public] && safe_dir?(option[:public])
161
+ path_info = Rack::Utils.unescape(env['PATH_INFO'])
162
+ path_info = '/' if path_info.to_s.empty?
162
163
 
163
164
  begin
164
165
  path_info_clean = path_info
165
- .encode(
166
- 'UTF-8', # Target encoding
167
- invalid: :replace, # Replace invalid byte sequences
168
- undef: :replace, # Replace characters undefined in UTF-8
169
- replace: '' # Use empty string for replacement
170
- )
171
- .gsub(%r{/$}, '') # Remove trailing slash, if present
172
- rescue ArgumentError => e
166
+ .encode(
167
+ 'UTF-8', # Target encoding
168
+ invalid: :replace, # Replace invalid byte sequences
169
+ undef: :replace, # Replace characters undefined in UTF-8
170
+ replace: '', # Use empty string for replacement
171
+ )
172
+ .gsub(%r{/$}, '') # Remove trailing slash, if present
173
+ rescue ArgumentError => ex
173
174
  # Log the error but don't expose details
174
- Otto.logger.error "[Otto.handle_request] Path encoding error"
175
- Otto.logger.debug "[Otto.handle_request] Error details: #{e.message}" if Otto.debug
175
+ Otto.logger.error '[Otto.handle_request] Path encoding error'
176
+ Otto.logger.debug "[Otto.handle_request] Error details: #{ex.message}" if Otto.debug
176
177
  # Set a default value or use the original path_info
177
178
  path_info_clean = path_info
178
179
  end
179
180
 
180
- base_path = File.split(path_info).first
181
+ base_path = File.split(path_info).first
181
182
  # Files in the root directory can refer to themselves
182
- base_path = path_info if base_path == '/'
183
- http_verb = env['REQUEST_METHOD'].upcase.to_sym
183
+ base_path = path_info if base_path == '/'
184
+ http_verb = env['REQUEST_METHOD'].upcase.to_sym
184
185
  literal_routes = routes_literal[http_verb] || {}
185
186
  literal_routes.merge! routes_literal[:GET] if http_verb == :HEAD
186
187
  if static_route && http_verb == :GET && routes_static[:GET].member?(base_path)
@@ -195,15 +196,15 @@ end
195
196
  routes_static[:GET][base_path] = base_path
196
197
  static_route.call(env)
197
198
  else
198
- extra_params = {}
199
- found_route = nil
200
- valid_routes = routes[http_verb] || []
199
+ extra_params = {}
200
+ found_route = nil
201
+ valid_routes = routes[http_verb] || []
201
202
  valid_routes.push(*routes[:GET]) if http_verb == :HEAD
202
203
  valid_routes.each do |route|
203
204
  # Otto.logger.debug " request: #{http_verb} #{path_info} (trying route: #{route.verb} #{route.pattern})"
204
205
  next unless (match = route.pattern.match(path_info))
205
206
 
206
- values = match.captures.to_a
207
+ values = match.captures.to_a
207
208
  # The first capture returned is the entire matched string b/c
208
209
  # we wrapped the entire regex in parens. We don't need it to
209
210
  # the full match.
@@ -222,7 +223,7 @@ end
222
223
  else
223
224
  {}
224
225
  end
225
- found_route = route
226
+ found_route = route
226
227
  break
227
228
  end
228
229
  found_route ||= literal_routes['/404']
@@ -245,7 +246,7 @@ end
245
246
  return if route.nil?
246
247
 
247
248
  local_params = params.clone
248
- local_path = route.path.clone
249
+ local_path = route.path.clone
249
250
 
250
251
  local_params.each_pair do |k, v|
251
252
  next unless local_path.match(":#{k}")
@@ -253,16 +254,19 @@ end
253
254
  local_path.gsub!(":#{k}", v.to_s)
254
255
  local_params.delete(k)
255
256
  end
256
- uri = Addressable::URI.new
257
- uri.path = local_path
258
- uri.query_values = local_params unless local_params.empty?
257
+
258
+ uri = URI::HTTP.new(nil, nil, nil, nil, nil, local_path, nil, nil, nil)
259
+ unless local_params.empty?
260
+ query_string = local_params.map { |k, v| "#{URI.encode_www_form_component(k)}=#{URI.encode_www_form_component(v)}" }.join('&')
261
+ uri.query = query_string
262
+ end
259
263
  uri.to_s
260
264
  end
261
265
 
262
266
  def determine_locale(env)
263
267
  accept_langs = env['HTTP_ACCEPT_LANGUAGE']
264
268
  accept_langs = option[:locale] if accept_langs.to_s.empty?
265
- locales = []
269
+ locales = []
266
270
  unless accept_langs.empty?
267
271
  locales = accept_langs.split(',').map do |l|
268
272
  l += ';q=1.0' unless /;q=\d+(?:\.\d+)?$/.match?(l)
@@ -282,7 +286,7 @@ end
282
286
  # @param middleware [Class] The middleware class to add
283
287
  # @param args [Array] Additional arguments for the middleware
284
288
  # @param block [Proc] Optional block for middleware configuration
285
- def use(middleware, *args, &block)
289
+ def use(middleware, *, &)
286
290
  @middleware_stack << middleware
287
291
  end
288
292
 
@@ -343,7 +347,7 @@ end
343
347
  # @param include_subdomains [Boolean] Apply to all subdomains (default: true)
344
348
  # @example
345
349
  # otto.enable_hsts!(max_age: 86400, include_subdomains: false)
346
- def enable_hsts!(max_age: 31536000, include_subdomains: true)
350
+ def enable_hsts!(max_age: 31_536_000, include_subdomains: true)
347
351
  @security_config.enable_hsts!(max_age: max_age, include_subdomains: include_subdomains)
348
352
  end
349
353
 
@@ -396,8 +400,12 @@ end
396
400
  Otto.logger.error "[#{error_id}] #{error.class}: #{error.message}"
397
401
  Otto.logger.debug "[#{error_id}] Backtrace: #{error.backtrace.join("\n")}" if Otto.debug
398
402
 
399
- # Check for custom error routes
400
- request = Rack::Request.new(env) rescue nil
403
+ # Parse request for content negotiation
404
+ begin
405
+ Rack::Request.new(env)
406
+ rescue StandardError
407
+ nil
408
+ end
401
409
  literal_routes = @routes_literal[:GET] || {}
402
410
 
403
411
  # Try custom 500 route first
@@ -405,11 +413,17 @@ end
405
413
  begin
406
414
  env['otto.error_id'] = error_id
407
415
  return found_route.call(env)
408
- rescue StandardError => route_error
409
- Otto.logger.error "[#{error_id}] Error in custom error handler: #{route_error.message}"
416
+ rescue StandardError => ex
417
+ Otto.logger.error "[#{error_id}] Error in custom error handler: #{ex.message}"
410
418
  end
411
419
  end
412
420
 
421
+ # Content negotiation for built-in error response
422
+ accept_header = env['HTTP_ACCEPT'].to_s
423
+ if accept_header.include?('application/json')
424
+ return json_error_response(error_id)
425
+ end
426
+
413
427
  # Fallback to built-in error response
414
428
  @server_error || secure_error_response(error_id)
415
429
  end
@@ -418,12 +432,35 @@ end
418
432
  body = if Otto.env?(:dev, :development)
419
433
  "Server error (ID: #{error_id}). Check logs for details."
420
434
  else
421
- "An error occurred. Please try again later."
435
+ 'An error occurred. Please try again later.'
422
436
  end
423
437
 
424
438
  headers = {
425
439
  'content-type' => 'text/plain',
426
- 'content-length' => body.bytesize.to_s
440
+ 'content-length' => body.bytesize.to_s,
441
+ }.merge(@security_config.security_headers)
442
+
443
+ [500, headers, [body]]
444
+ end
445
+
446
+ def json_error_response(error_id)
447
+ error_data = if Otto.env?(:dev, :development)
448
+ {
449
+ error: 'Internal Server Error',
450
+ message: 'Server error occurred. Check logs for details.',
451
+ error_id: error_id,
452
+ }
453
+ else
454
+ {
455
+ error: 'Internal Server Error',
456
+ message: 'An error occurred. Please try again later.',
457
+ }
458
+ end
459
+
460
+ body = JSON.generate(error_data)
461
+ headers = {
462
+ 'content-type' => 'application/json',
463
+ 'content-length' => body.bytesize.to_s,
427
464
  }.merge(@security_config.security_headers)
428
465
 
429
466
  [500, headers, [body]]
data/otto.gemspec CHANGED
@@ -3,17 +3,17 @@
3
3
  require_relative 'lib/otto/version'
4
4
 
5
5
  Gem::Specification.new do |spec|
6
- spec.name = 'otto'
7
- spec.version = Otto::VERSION.to_s
8
- spec.summary = 'Auto-define your rack-apps in plaintext.'
9
- spec.description = "Otto: #{spec.summary}"
10
- spec.email = 'gems@solutious.com'
11
- spec.authors = ['Delano Mandelbaum']
12
- spec.license = 'MIT'
13
- spec.files = Dir.chdir(File.expand_path(__dir__)) do
6
+ spec.name = 'otto'
7
+ spec.version = Otto::VERSION.to_s
8
+ spec.summary = 'Auto-define your rack-apps in plaintext.'
9
+ spec.description = "Otto: #{spec.summary}"
10
+ spec.email = 'gems@solutious.com'
11
+ spec.authors = ['Delano Mandelbaum']
12
+ spec.license = 'MIT'
13
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
14
14
  `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
15
15
  end
16
- spec.homepage = 'https://github.com/delano/otto'
16
+ spec.homepage = 'https://github.com/delano/otto'
17
17
  spec.require_paths = ['lib']
18
18
 
19
19
  spec.required_ruby_version = ['>= 3.4', '< 4.0']
@@ -21,7 +21,6 @@ Gem::Specification.new do |spec|
21
21
  # https://github.com/delano/otto/security/dependabot/5
22
22
  spec.add_dependency 'rexml', '>= 3.3.6'
23
23
 
24
- spec.add_dependency 'addressable', '~> 2.2', '< 3'
25
24
  spec.add_dependency 'rack', '~> 3.1', '< 4.0'
26
25
  spec.add_dependency 'rack-parser', '~> 0.7'
27
26
  spec.metadata['rubygems_mfa_required'] = 'true'
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.2.0
4
+ version: 1.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Delano Mandelbaum
@@ -23,26 +23,6 @@ dependencies:
23
23
  - - ">="
24
24
  - !ruby/object:Gem::Version
25
25
  version: 3.3.6
26
- - !ruby/object:Gem::Dependency
27
- name: addressable
28
- requirement: !ruby/object:Gem::Requirement
29
- requirements:
30
- - - "~>"
31
- - !ruby/object:Gem::Version
32
- version: '2.2'
33
- - - "<"
34
- - !ruby/object:Gem::Version
35
- version: '3'
36
- type: :runtime
37
- prerelease: false
38
- version_requirements: !ruby/object:Gem::Requirement
39
- requirements:
40
- - - "~>"
41
- - !ruby/object:Gem::Version
42
- version: '2.2'
43
- - - "<"
44
- - !ruby/object:Gem::Version
45
- version: '3'
46
26
  - !ruby/object:Gem::Dependency
47
27
  name: rack
48
28
  requirement: !ruby/object:Gem::Requirement
@@ -86,12 +66,10 @@ files:
86
66
  - ".gitignore"
87
67
  - ".rspec"
88
68
  - ".rubocop.yml"
89
- - ".rubocop_todo.yml"
90
69
  - Gemfile
91
70
  - Gemfile.lock
92
71
  - LICENSE.txt
93
72
  - README.md
94
- - VERSION.yml
95
73
  - examples/basic/app.rb
96
74
  - examples/basic/config.ru
97
75
  - examples/basic/routes