hanami-controller 1.3.3 → 2.0.0.alpha4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +82 -0
  3. data/LICENSE.md +1 -1
  4. data/README.md +299 -537
  5. data/hanami-controller.gemspec +4 -3
  6. data/lib/hanami/action/application_action.rb +131 -0
  7. data/lib/hanami/action/application_configuration/content_security_policy.rb +118 -0
  8. data/lib/hanami/action/application_configuration/cookies.rb +29 -0
  9. data/lib/hanami/action/application_configuration/sessions.rb +46 -0
  10. data/lib/hanami/action/application_configuration.rb +90 -0
  11. data/lib/hanami/action/base_params.rb +2 -2
  12. data/lib/hanami/action/cache/cache_control.rb +4 -4
  13. data/lib/hanami/action/cache/conditional_get.rb +3 -1
  14. data/lib/hanami/action/cache/directives.rb +1 -1
  15. data/lib/hanami/action/cache/expires.rb +3 -3
  16. data/lib/hanami/action/cache.rb +1 -139
  17. data/lib/hanami/action/configuration.rb +428 -0
  18. data/lib/hanami/action/cookie_jar.rb +3 -3
  19. data/lib/hanami/action/cookies.rb +3 -62
  20. data/lib/hanami/action/csrf_protection.rb +214 -0
  21. data/lib/hanami/action/flash.rb +102 -207
  22. data/lib/hanami/action/glue.rb +5 -31
  23. data/lib/hanami/action/halt.rb +12 -0
  24. data/lib/hanami/action/mime.rb +78 -485
  25. data/lib/hanami/action/params.rb +2 -2
  26. data/lib/hanami/action/rack/file.rb +1 -1
  27. data/lib/hanami/action/request.rb +30 -20
  28. data/lib/hanami/action/response.rb +193 -0
  29. data/lib/hanami/action/session.rb +11 -128
  30. data/lib/hanami/action/standalone_action.rb +578 -0
  31. data/lib/hanami/action/validatable.rb +1 -1
  32. data/lib/hanami/action/view_name_inferrer.rb +46 -0
  33. data/lib/hanami/action.rb +129 -73
  34. data/lib/hanami/controller/version.rb +1 -1
  35. data/lib/hanami/controller.rb +0 -227
  36. data/lib/hanami/http/status.rb +2 -2
  37. metadata +44 -26
  38. data/lib/hanami/action/callable.rb +0 -92
  39. data/lib/hanami/action/callbacks.rb +0 -214
  40. data/lib/hanami/action/configurable.rb +0 -50
  41. data/lib/hanami/action/exposable/guard.rb +0 -104
  42. data/lib/hanami/action/exposable.rb +0 -126
  43. data/lib/hanami/action/head.rb +0 -121
  44. data/lib/hanami/action/rack/callable.rb +0 -47
  45. data/lib/hanami/action/rack/errors.rb +0 -53
  46. data/lib/hanami/action/rack.rb +0 -411
  47. data/lib/hanami/action/redirect.rb +0 -59
  48. data/lib/hanami/action/throwable.rb +0 -169
  49. data/lib/hanami/controller/configuration.rb +0 -763
  50. data/lib/hanami-controller.rb +0 -1
@@ -17,13 +17,14 @@ Gem::Specification.new do |spec|
17
17
  spec.executables = []
18
18
  spec.test_files = spec.files.grep(%r{^(spec)/})
19
19
  spec.require_paths = ['lib']
20
- spec.required_ruby_version = '>= 2.3.0'
20
+ spec.required_ruby_version = '>= 2.6.0'
21
21
 
22
22
  spec.add_dependency 'rack', '~> 2.0'
23
- spec.add_dependency 'hanami-utils', '~> 1.3'
23
+ spec.add_dependency 'hanami-utils', '~> 2.0.alpha'
24
+ spec.add_dependency 'dry-configurable', '~> 0.13', '>= 0.13.0'
24
25
 
25
26
  spec.add_development_dependency 'bundler', '>= 1.6', '< 3'
26
27
  spec.add_development_dependency 'rack-test', '~> 1.0'
27
28
  spec.add_development_dependency 'rake', '~> 13'
28
- spec.add_development_dependency 'rspec', '~> 3.7'
29
+ spec.add_development_dependency 'rspec', '~> 3.9'
29
30
  end
@@ -0,0 +1,131 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hanami
4
+ class Action
5
+ class ApplicationAction < Module
6
+ attr_reader :provider
7
+ attr_reader :application
8
+
9
+ def initialize(provider)
10
+ @provider = provider
11
+ @application = provider.respond_to?(:application) ? provider.application : Hanami.application
12
+ end
13
+
14
+ def included(action_class)
15
+ action_class.include InstanceMethods
16
+
17
+ define_initialize
18
+ configure_action action_class
19
+ extend_behavior action_class
20
+ end
21
+
22
+ def inspect
23
+ "#<#{self.class.name}[#{provider.name}]>"
24
+ end
25
+
26
+ private
27
+
28
+ def define_initialize
29
+ resolve_view = method(:resolve_paired_view)
30
+ resolve_context = method(:resolve_view_context)
31
+ resolve_routes = method(:resolve_routes)
32
+
33
+ define_method :initialize do |**deps|
34
+ # Conditionally assign these to repsect any explictly auto-injected
35
+ # dependencies provided by the class
36
+ @view ||= deps[:view] || resolve_view.(self.class)
37
+ @view_context ||= deps[:view_context] || resolve_context.()
38
+ @routes ||= deps[:routes] || resolve_routes.()
39
+
40
+ super(**deps)
41
+ end
42
+ end
43
+
44
+ def resolve_paired_view(action_class)
45
+ view_identifiers = application.config.actions.view_name_inferrer.(
46
+ action_name: action_class.name,
47
+ provider: provider
48
+ )
49
+
50
+ view_identifiers.detect { |identifier|
51
+ break provider[identifier] if provider.key?(identifier)
52
+ }
53
+ end
54
+
55
+ def resolve_view_context
56
+ identifier = application.config.actions.view_context_identifier
57
+
58
+ if provider.key?(identifier)
59
+ provider[identifier]
60
+ elsif application.key?(identifier)
61
+ application[identifier]
62
+ end
63
+ end
64
+
65
+ def resolve_routes
66
+ application[:routes_helper] if application.key?(:routes_helper)
67
+ end
68
+
69
+ def configure_action(action_class)
70
+ action_class.config.settings.each do |setting|
71
+ application_value = application.config.actions.public_send(:"#{setting}")
72
+ action_class.config.public_send :"#{setting}=", application_value
73
+ end
74
+ end
75
+
76
+ def extend_behavior(action_class)
77
+ if application.config.actions.sessions.enabled?
78
+ require "hanami/action/session"
79
+ action_class.include Hanami::Action::Session
80
+ end
81
+
82
+ if application.config.actions.csrf_protection
83
+ require "hanami/action/csrf_protection"
84
+ action_class.include Hanami::Action::CSRFProtection
85
+ end
86
+
87
+ if application.config.actions.cookies.enabled?
88
+ require "hanami/action/cookies"
89
+ action_class.include Hanami::Action::Cookies
90
+ end
91
+ end
92
+
93
+ module InstanceMethods
94
+ attr_reader :view
95
+ attr_reader :view_context
96
+ attr_reader :routes
97
+
98
+ def build_response(**options)
99
+ options = options.merge(view_options: method(:view_options))
100
+ super(**options)
101
+ end
102
+
103
+ def view_options(req, res)
104
+ {context: view_context&.with(**view_context_options(req, res))}.compact
105
+ end
106
+
107
+ def view_context_options(req, res)
108
+ {request: req, response: res}
109
+ end
110
+
111
+ def finish(req, res, halted)
112
+ res.render(view, **req.params) if render?(res)
113
+ super
114
+ end
115
+
116
+ # Decide whether to render the current response with the associated view.
117
+ # This can be overridden to enable/disable automatic rendering.
118
+ #
119
+ # @param res [Hanami::Action::Response]
120
+ #
121
+ # @return [TrueClass,FalseClass]
122
+ #
123
+ # @since 2.0.0
124
+ # @api public
125
+ def render?(res)
126
+ view && res.body.empty?
127
+ end
128
+ end
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hanami
4
+ class Action
5
+ class ApplicationConfiguration
6
+ # Configuration for Content Security Policy in Hanami applications
7
+ #
8
+ # @since 2.0.0
9
+ class ContentSecurityPolicy
10
+ # @since 2.0.0
11
+ # @api private
12
+ def initialize(&blk)
13
+ @policy = {
14
+ base_uri: "'self'",
15
+ child_src: "'self'",
16
+ connect_src: "'self'",
17
+ default_src: "'none'",
18
+ font_src: "'self'",
19
+ form_action: "'self'",
20
+ frame_ancestors: "'self'",
21
+ frame_src: "'self'",
22
+ img_src: "'self' https: data:",
23
+ media_src: "'self'",
24
+ object_src: "'none'",
25
+ plugin_types: "application/pdf",
26
+ script_src: "'self'",
27
+ style_src: "'self' 'unsafe-inline' https:"
28
+ }
29
+
30
+ blk&.(self)
31
+ end
32
+
33
+ # @since 2.0.0
34
+ # @api private
35
+ def initialize_copy(original_object)
36
+ @policy = original_object.instance_variable_get(:@policy).dup
37
+ super
38
+ end
39
+
40
+ # Get a CSP setting
41
+ #
42
+ # @param key [Symbol] the underscored name of the CPS setting
43
+ # @return [String,NilClass] the CSP setting, if any
44
+ #
45
+ # @since 2.0.0
46
+ # @api public
47
+ #
48
+ # @example
49
+ # module MyApp
50
+ # class Application < Hanami::Application
51
+ # config.actions.content_security_policy[:base_uri] # => "'self'"
52
+ # end
53
+ # end
54
+ def [](key)
55
+ @policy[key]
56
+ end
57
+
58
+ # Set a CSP setting
59
+ #
60
+ # @param key [Symbol] the underscored name of the CPS setting
61
+ # @param value [String] the CSP setting value
62
+ #
63
+ # @since 2.0.0
64
+ # @api public
65
+ #
66
+ # @example Replace a default value
67
+ # module MyApp
68
+ # class Application < Hanami::Application
69
+ # config.actions.content_security_policy[:plugin_types] = nil
70
+ # end
71
+ # end
72
+ #
73
+ # @example Append to a default value
74
+ # module MyApp
75
+ # class Application < Hanami::Application
76
+ # config.actions.content_security_policy[:script_src] += " https://my.cdn.test"
77
+ # end
78
+ # end
79
+ def []=(key, value)
80
+ @policy[key] = value
81
+ end
82
+
83
+ # Deletes a CSP key
84
+ #
85
+ # @param key [Symbol] the underscored name of the CPS setting
86
+ #
87
+ # @since 2.0.0
88
+ # @api public
89
+ #
90
+ # @example
91
+ # module MyApp
92
+ # class Application < Hanami::Application
93
+ # config.actions.content_security_policy.delete(:object_src)
94
+ # end
95
+ # end
96
+ def delete(key)
97
+ @policy.delete(key)
98
+ end
99
+
100
+ # @since 2.0.0
101
+ # @api private
102
+ def to_str
103
+ @policy.map do |key, value|
104
+ "#{dasherize(key)} #{value}"
105
+ end.join(";\n")
106
+ end
107
+
108
+ private
109
+
110
+ # @since 2.0.0
111
+ # @api private
112
+ def dasherize(key)
113
+ key.to_s.gsub("_", "-")
114
+ end
115
+ end
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hanami
4
+ class Action
5
+ class ApplicationConfiguration
6
+ # Wrapper for application-level configuration of HTTP cookies for Hanami actions.
7
+ # This decorates the hash of cookie options that is otherwise directly configurable
8
+ # on actions, and adds the `enabled?` method to allow `ApplicationAction` to
9
+ # determine whether to include the `Action::Cookies` module.
10
+ #
11
+ # @since 2.0.0
12
+ class Cookies
13
+ attr_reader :options
14
+
15
+ def initialize(options)
16
+ @options = options
17
+ end
18
+
19
+ def enabled?
20
+ !options.nil?
21
+ end
22
+
23
+ def to_h
24
+ options.to_h
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/core/constants"
4
+ require "hanami/utils/string"
5
+ require "hanami/utils/class"
6
+
7
+ module Hanami
8
+ class Action
9
+ class ApplicationConfiguration
10
+ # Configuration for HTTP sessions in Hanami actions
11
+ #
12
+ # @since 2.0.0
13
+ class Sessions
14
+ attr_reader :storage, :options
15
+
16
+ def initialize(storage = nil, *options)
17
+ @storage = storage
18
+ @options = options
19
+ end
20
+
21
+ def enabled?
22
+ !storage.nil?
23
+ end
24
+
25
+ def middleware
26
+ return [] if !enabled?
27
+
28
+ [[storage_middleware, options]]
29
+ end
30
+
31
+ private
32
+
33
+ def storage_middleware
34
+ require_storage
35
+
36
+ name = Utils::String.classify(storage)
37
+ Utils::Class.load!(name, ::Rack::Session)
38
+ end
39
+
40
+ def require_storage
41
+ require "rack/session/#{storage}"
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "application_configuration/cookies"
4
+ require_relative "application_configuration/sessions"
5
+ require_relative "application_configuration/content_security_policy"
6
+ require_relative "configuration"
7
+ require_relative "view_name_inferrer"
8
+
9
+ module Hanami
10
+ class Action
11
+ class ApplicationConfiguration
12
+ include Dry::Configurable
13
+
14
+ setting :cookies, default: {}, constructor: -> options { Cookies.new(options) }
15
+ setting :sessions, constructor: proc { |storage, *options| Sessions.new(storage, *options) }
16
+ setting :csrf_protection
17
+
18
+ setting :name_inference_base, default: "actions"
19
+ setting :view_context_identifier, default: "view.context"
20
+ setting :view_name_inferrer, default: ViewNameInferrer
21
+ setting :view_name_inference_base, default: "views"
22
+
23
+ attr_accessor :content_security_policy
24
+
25
+ def initialize(*, **options)
26
+ super()
27
+
28
+ @base_configuration = Configuration.new
29
+ @content_security_policy = ContentSecurityPolicy.new do |csp|
30
+ if assets_server_url = options[:assets_server_url]
31
+ csp[:script_src] += " #{assets_server_url}"
32
+ csp[:style_src] += " #{assets_server_url}"
33
+ end
34
+ end
35
+
36
+ configure_defaults
37
+ end
38
+
39
+ def finalize!
40
+ # A nil value for `csrf_protection` means it has not been explicitly configured
41
+ # (neither true nor false), so we can default it to whether sessions are enabled
42
+ self.csrf_protection = sessions.enabled? if csrf_protection.nil?
43
+
44
+ if self.content_security_policy
45
+ self.default_headers["Content-Security-Policy"] = self.content_security_policy.to_str
46
+ end
47
+ end
48
+
49
+ # Returns the list of available settings
50
+ #
51
+ # @return [Set]
52
+ #
53
+ # @since 2.0.0
54
+ # @api private
55
+ def settings
56
+ base_configuration.settings + self.class.settings
57
+ end
58
+
59
+ private
60
+
61
+ attr_reader :base_configuration
62
+
63
+ # Apply defaults for base configuration settings
64
+ def configure_defaults
65
+ self.default_request_format = :html
66
+ self.default_response_format = :html
67
+
68
+ self.default_headers = {
69
+ "X-Frame-Options" => "DENY",
70
+ "X-Content-Type-Options" => "nosniff",
71
+ "X-XSS-Protection" => "1; mode=block"
72
+ }
73
+ end
74
+
75
+ def method_missing(name, *args, &block)
76
+ if config.respond_to?(name)
77
+ config.public_send(name, *args, &block)
78
+ elsif base_configuration.respond_to?(name)
79
+ base_configuration.public_send(name, *args, &block)
80
+ else
81
+ super
82
+ end
83
+ end
84
+
85
+ def respond_to_missing?(name, _incude_all = false)
86
+ config.respond_to?(name) || base_configuration.respond_to?(name) || super
87
+ end
88
+ end
89
+ end
90
+ end
@@ -2,7 +2,7 @@ require 'rack/request'
2
2
  require 'hanami/utils/hash'
3
3
 
4
4
  module Hanami
5
- module Action
5
+ class Action
6
6
  class BaseParams
7
7
  # The key that returns raw input from the Rack env
8
8
  #
@@ -173,7 +173,7 @@ module Hanami
173
173
  def _router_params(fallback = {})
174
174
  env.fetch(ROUTER_PARAMS) do
175
175
  if session = fallback.delete(RACK_SESSION) # rubocop:disable Lint/AssignmentInCondition
176
- fallback[RACK_SESSION] = Utils::Hash.new(session).symbolize!.to_hash
176
+ fallback[RACK_SESSION] = Utils::Hash.deep_symbolize(session)
177
177
  end
178
178
 
179
179
  fallback
@@ -1,7 +1,7 @@
1
1
  require 'hanami/action/cache/directives'
2
2
 
3
3
  module Hanami
4
- module Action
4
+ class Action
5
5
  module Cache
6
6
  # Module with Cache-Control logic
7
7
  #
@@ -37,7 +37,7 @@ module Hanami
37
37
  def cache_control_directives
38
38
  @cache_control_directives || Object.new.tap do |null_object|
39
39
  def null_object.headers
40
- Hash.new
40
+ ::Hash.new
41
41
  end
42
42
  end
43
43
  end
@@ -49,9 +49,9 @@ module Hanami
49
49
  # @api private
50
50
  #
51
51
  # @see Hanami::Action#finish
52
- def finish
52
+ def finish(_, res, _)
53
+ res.headers.merge!(self.class.cache_control_directives.headers) unless res.headers.include? HEADER
53
54
  super
54
- headers.merge!(self.class.cache_control_directives.headers) unless headers.include? HEADER
55
55
  end
56
56
 
57
57
  # Class which stores CacheControl values
@@ -1,5 +1,7 @@
1
+ require "hanami/utils/blank"
2
+
1
3
  module Hanami
2
- module Action
4
+ class Action
3
5
  module Cache
4
6
  # @since 0.3.0
5
7
  # @api private
@@ -1,5 +1,5 @@
1
1
  module Hanami
2
- module Action
2
+ class Action
3
3
  module Cache
4
4
  # Cache-Control directives which have values
5
5
  #
@@ -1,7 +1,7 @@
1
1
  require 'hanami/action/cache/cache_control'
2
2
 
3
3
  module Hanami
4
- module Action
4
+ class Action
5
5
  module Cache
6
6
  # Module with Expires logic
7
7
  #
@@ -49,9 +49,9 @@ module Hanami
49
49
  # @api private
50
50
  #
51
51
  # @see Hanami::Action#finish
52
- def finish
52
+ def finish(_, res, _)
53
+ res.headers.merge!(self.class.expires_directives.headers) unless res.headers.include? HEADER
53
54
  super
54
- headers.merge!(self.class.expires_directives.headers) unless headers.include? HEADER
55
55
  end
56
56
 
57
57
  # Class which stores Expires directives
@@ -3,7 +3,7 @@ require 'hanami/action/cache/expires'
3
3
  require 'hanami/action/cache/conditional_get'
4
4
 
5
5
  module Hanami
6
- module Action
6
+ class Action
7
7
  # Cache type API
8
8
  #
9
9
  # @since 0.3.0
@@ -26,144 +26,6 @@ module Hanami
26
26
  include CacheControl, Expires
27
27
  end
28
28
  end
29
-
30
- protected
31
-
32
- # Specify response freshness policy for HTTP caches (Cache-Control header).
33
- # Any number of non-value directives (:public, :private, :no_cache,
34
- # :no_store, :must_revalidate, :proxy_revalidate) may be passed along with
35
- # a Hash of value directives (:max_age, :min_stale, :s_max_age).
36
- #
37
- # See RFC 2616 / 14.9 for more on standard cache control directives:
38
- # http://tools.ietf.org/html/rfc2616#section-14.9.1
39
- #
40
- # @param values [Array<Symbols, Hash>] mapped to cache_control directives
41
- # @option values [Symbol] :public
42
- # @option values [Symbol] :private
43
- # @option values [Symbol] :no_cache
44
- # @option values [Symbol] :no_store
45
- # @option values [Symbol] :must_validate
46
- # @option values [Symbol] :proxy_revalidate
47
- # @option values [Hash] :max_age
48
- # @option values [Hash] :min_stale
49
- # @option values [Hash] :s_max_age
50
- #
51
- # @return void
52
- #
53
- # @since 0.3.0
54
- #
55
- # @example
56
- # require 'hanami/controller'
57
- # require 'hanami/action/cache'
58
- #
59
- # class Show
60
- # include Hanami::Action
61
- # include Hanami::Action::Cache
62
- #
63
- # def call(params)
64
- # # ...
65
- #
66
- # # set Cache-Control directives
67
- # cache_control :public, max_age: 900, s_maxage: 86400
68
- #
69
- # # overwrite previous Cache-Control directives
70
- # cache_control :private, :no_cache, :no_store
71
- #
72
- # => Cache-Control: private, no-store, max-age=900
73
- #
74
- # end
75
- # end
76
- def cache_control(*values)
77
- cache_control = CacheControl::Directives.new(*values)
78
- headers.merge!(cache_control.headers)
79
- end
80
-
81
- # Set the Expires header and Cache-Control/max-age directive. Amount
82
- # can be an integer number of seconds in the future or a Time object
83
- # indicating when the response should be considered "stale". The remaining
84
- # "values" arguments are passed to the #cache_control helper:
85
- #
86
- # @param amount [Integer,Time] number of seconds or point in time
87
- # @param values [Array<Symbols>] mapped to cache_control directives
88
- #
89
- # @return void
90
- #
91
- # @since 0.3.0
92
- #
93
- # @example
94
- # require 'hanami/controller'
95
- # require 'hanami/action/cache'
96
- #
97
- # class Show
98
- # include Hanami::Action
99
- # include Hanami::Action::Cache
100
- #
101
- # def call(params)
102
- # # ...
103
- #
104
- # # set Cache-Control directives and Expires
105
- # expires 900, :public
106
- #
107
- # # overwrite Cache-Control directives and Expires
108
- # expires 300, :private, :no_cache, :no_store
109
- #
110
- # => Expires: Thu, 26 Jun 2014 12:00:00 GMT
111
- # => Cache-Control: private, no-cache, no-store max-age=300
112
- #
113
- # end
114
- # end
115
- def expires(amount, *values)
116
- expires = Expires::Directives.new(amount, *values)
117
- headers.merge!(expires.headers)
118
- end
119
-
120
- # Set the etag, last_modified, or both headers on the response
121
- # and halts a 304 Not Modified if the request is still fresh
122
- # respecting IfNoneMatch and IfModifiedSince request headers
123
- #
124
- # @param options [Hash]
125
- # @option options [Integer] :etag for testing IfNoneMatch conditions
126
- # @option options [Date] :last_modified for testing IfModifiedSince conditions
127
- #
128
- # @return void
129
- #
130
- # @since 0.3.0
131
- #
132
- # @example
133
- # require 'hanami/controller'
134
- # require 'hanami/action/cache'
135
- #
136
- # class Show
137
- # include Hanami::Action
138
- # include Hanami::Action::Cache
139
- #
140
- # def call(params)
141
- # # ...
142
- #
143
- # # set etag response header and halt 304
144
- # # if request matches IF_NONE_MATCH header
145
- # fresh etag: @resource.updated_at.to_i
146
- #
147
- # # set last_modified response header and halt 304
148
- # # if request matches IF_MODIFIED_SINCE
149
- # fresh last_modified: @resource.updated_at
150
- #
151
- # # set etag and last_modified response header,
152
- # # halt 304 if request matches IF_MODIFIED_SINCE
153
- # # and IF_NONE_MATCH
154
- # fresh last_modified: @resource.updated_at
155
- #
156
- # end
157
- # end
158
- def fresh(options)
159
- conditional_get = ConditionalGet.new(@_env, options)
160
-
161
- headers.merge!(conditional_get.headers)
162
-
163
- conditional_get.fresh? do
164
- halt 304
165
- end
166
- end
167
29
  end
168
30
  end
169
31
  end