ey-hmac 2.3.0 → 2.4.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 780a6fcc90418a0389166a31e520337dced06873038bb0a9e71a6f3b70996835
4
- data.tar.gz: 073f26a6c11bd06036e23a49b50e8ffea15ca70c608d05e0e7be1a0b7547cda7
3
+ metadata.gz: 1ea57d603aa108a1bf510fcf6e61aa45b1d1d0fca0ba7aa1b97681396fae9a7c
4
+ data.tar.gz: 7ad6caf49126be2b2a4b75acbc97ecf9d1898307a896f909493850be26d8261e
5
5
  SHA512:
6
- metadata.gz: f0b5f2b1827e7a180b35ffa2d3b2e2918a470d008735dbb5b5b79430ac2201c7d58a5abcf124f5ee69119ab83b2b882f9802b70e0f00208df130f9974fc82e2f
7
- data.tar.gz: 8d5b959e371034a6a436f7bfec4602e4e477a9830b6d67a6fc2e23378d5002fb2fbbd3714a635b8efe579bcf4f07316176be42ae90985317a86f72e2d7c57a15
6
+ metadata.gz: 6c790db93637c36fe752759cdb1aac1a70d3f1378ca5c2ae6b4132b8115d97b5e75b15e75de7dc069800980846d83524dbbe58903d9fd4b2006499e0ff8f9826
7
+ data.tar.gz: 6d8745e27b9c8d01d01d9f6bc608eba2f6fd6f8476a28eba9097dda9765944b8d5ba535be0901f151e69f4b66b9e1a2d022bdd29cbc38fc68a3ec9cacfaf2500
@@ -0,0 +1,70 @@
1
+ # For most projects, this workflow file will not need changing; you simply need
2
+ # to commit it to your repository.
3
+ #
4
+ # You may wish to alter this file to override the set of languages analyzed,
5
+ # or to provide custom queries or build logic.
6
+ #
7
+ # ******** NOTE ********
8
+ # We have attempted to detect the languages in your repository. Please check
9
+ # the `language` matrix defined below to confirm you have the correct set of
10
+ # supported CodeQL languages.
11
+ #
12
+ name: "CodeQL"
13
+
14
+ on:
15
+ push:
16
+ branches: [ master ]
17
+ pull_request:
18
+ # The branches below must be a subset of the branches above
19
+ branches: [ master ]
20
+ schedule:
21
+ - cron: '36 16 * * 0'
22
+
23
+ jobs:
24
+ analyze:
25
+ name: Analyze
26
+ runs-on: ubuntu-latest
27
+ permissions:
28
+ actions: read
29
+ contents: read
30
+ security-events: write
31
+
32
+ strategy:
33
+ fail-fast: false
34
+ matrix:
35
+ language: [ 'ruby' ]
36
+ # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
37
+ # Learn more about CodeQL language support at https://git.io/codeql-language-support
38
+
39
+ steps:
40
+ - name: Checkout repository
41
+ uses: actions/checkout@v2
42
+
43
+ # Initializes the CodeQL tools for scanning.
44
+ - name: Initialize CodeQL
45
+ uses: github/codeql-action/init@v1
46
+ with:
47
+ languages: ${{ matrix.language }}
48
+ # If you wish to specify custom queries, you can do so here or in a config file.
49
+ # By default, queries listed here will override any specified in a config file.
50
+ # Prefix the list here with "+" to use these queries and those in the config file.
51
+ # queries: ./path/to/local/query, your-org/your-repo/queries@main
52
+
53
+ # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
54
+ # If this step fails, then you should remove it and run the build manually (see below)
55
+ - name: Autobuild
56
+ uses: github/codeql-action/autobuild@v1
57
+
58
+ # ℹ️ Command-line programs to run using the OS shell.
59
+ # 📚 https://git.io/JvXDl
60
+
61
+ # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
62
+ # and modify them (or add more) to build your code if your project
63
+ # uses a compiled language
64
+
65
+ #- run: |
66
+ # make bootstrap
67
+ # make release
68
+
69
+ - name: Perform CodeQL Analysis
70
+ uses: github/codeql-action/analyze@v1
@@ -31,5 +31,7 @@ jobs:
31
31
  with:
32
32
  ruby-version: ${{ matrix.ruby-version }}
33
33
  bundler-cache: true # runs 'bundle install' and caches installed gems automatically
34
+ - name: Lint
35
+ run: bundle exec rubocop -D
34
36
  - name: Run tests
35
37
  run: bundle exec rspec
data/.rubocop.yml ADDED
@@ -0,0 +1,31 @@
1
+ inherit_from: .rubocop_todo.yml
2
+
3
+ # The behavior of RuboCop can be controlled via the .rubocop.yml
4
+ # configuration file. It makes it possible to enable/disable
5
+ # certain cops (checks) and to alter their behavior if they accept
6
+ # any parameters. The file can be placed either in your home
7
+ # directory or in some project directory.
8
+ #
9
+ # RuboCop will start looking for the configuration file in the directory
10
+ # where the inspected file is and continue its way up to the root directory.
11
+ #
12
+ # See https://docs.rubocop.org/rubocop/configuration
13
+ AllCops:
14
+ SuggestExtensions: false
15
+ NewCops: 'disable'
16
+ require:
17
+ - rubocop-rspec
18
+ Metrics/BlockLength:
19
+ Enabled: false
20
+ Metrics/AbcSize:
21
+ Enabled: false
22
+ Style/ClassAndModuleChildren:
23
+ EnforcedStyle: compact
24
+ Metrics/MethodLength:
25
+ Enabled: false
26
+ Metrics/ClassLength:
27
+ Enabled: false
28
+ RSpec/ExampleLength:
29
+ Enabled: false
30
+ RSpec/MultipleExpectations:
31
+ Enabled: false
data/.rubocop_todo.yml ADDED
@@ -0,0 +1,82 @@
1
+ # This configuration was generated by
2
+ # `rubocop --auto-gen-config --auto-gen-only-exclude`
3
+ # on 2022-02-08 03:20:19 UTC using RuboCop version 1.25.1.
4
+ # The point is for the user to remove these configuration records
5
+ # one by one as the offenses are removed from the code base.
6
+ # Note that changes in the inspected code, or installation of new
7
+ # versions of RuboCop, may require this file to be generated again.
8
+
9
+ # Offense count: 1
10
+ # Configuration parameters: Include.
11
+ # Include: **/*.gemspec
12
+ Gemspec/RequiredRubyVersion:
13
+ Exclude:
14
+ - 'ey-hmac.gemspec'
15
+
16
+ # Offense count: 1
17
+ # Configuration parameters: ExpectMatchingDefinition, CheckDefinitionPathHierarchy, CheckDefinitionPathHierarchyRoots, Regex, IgnoreExecutableScripts, AllowedAcronyms.
18
+ # CheckDefinitionPathHierarchyRoots: lib, spec, test, src
19
+ # AllowedAcronyms: CLI, DSL, ACL, API, ASCII, CPU, CSS, DNS, EOF, GUID, HTML, HTTP, HTTPS, ID, IP, JSON, LHS, QPS, RAM, RHS, RPC, SLA, SMTP, SQL, SSH, TCP, TLS, TTL, UDP, UI, UID, UUID, URI, URL, UTF8, VM, XML, XMPP, XSRF, XSS
20
+ Naming/FileName:
21
+ Exclude:
22
+ - 'lib/ey-hmac.rb'
23
+
24
+ # Offense count: 2
25
+ # Configuration parameters: MinNameLength, AllowNamesEndingInNumbers, AllowedNames, ForbiddenNames.
26
+ # AllowedNames: at, by, db, id, in, io, ip, of, on, os, pp, to
27
+ Naming/MethodParameterName:
28
+ Exclude:
29
+ - 'lib/ey-hmac/adapter.rb'
30
+
31
+ # Offense count: 2
32
+ RSpec/BeforeAfterAll:
33
+ Exclude:
34
+ - 'spec/spec_helper.rb'
35
+ - 'spec/rails_helper.rb'
36
+ - 'spec/support/**/*.rb'
37
+ - 'spec/faraday_spec.rb'
38
+ - 'spec/rack_spec.rb'
39
+
40
+ # Offense count: 2
41
+ # Configuration parameters: IgnoredMetadata.
42
+ RSpec/DescribeClass:
43
+ Exclude:
44
+ - '**/spec/features/**/*'
45
+ - '**/spec/requests/**/*'
46
+ - '**/spec/routing/**/*'
47
+ - '**/spec/system/**/*'
48
+ - '**/spec/views/**/*'
49
+ - 'spec/faraday_spec.rb'
50
+ - 'spec/rack_spec.rb'
51
+
52
+ # Offense count: 1
53
+ RSpec/LetSetup:
54
+ Exclude:
55
+ - 'spec/faraday_spec.rb'
56
+
57
+ # Offense count: 4
58
+ # Configuration parameters: AllowedConstants.
59
+ Style/Documentation:
60
+ Exclude:
61
+ - 'spec/**/*'
62
+ - 'test/**/*'
63
+ - 'lib/ey-hmac.rb'
64
+ - 'lib/ey-hmac/adapter/faraday.rb'
65
+ - 'lib/ey-hmac/adapter/rack.rb'
66
+ - 'lib/ey-hmac/faraday.rb'
67
+
68
+ # Offense count: 3
69
+ # Configuration parameters: MinBodyLength.
70
+ Style/GuardClause:
71
+ Exclude:
72
+ - 'lib/ey-hmac/adapter.rb'
73
+ - 'lib/ey-hmac/adapter/faraday.rb'
74
+ - 'lib/ey-hmac/adapter/rack.rb'
75
+
76
+ # Offense count: 2
77
+ # Cop supports --auto-correct.
78
+ # Configuration parameters: Max, AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns.
79
+ # URISchemes: http, https
80
+ Layout/LineLength:
81
+ Exclude:
82
+ - 'lib/ey-hmac/adapter.rb'
data/Gemfile CHANGED
@@ -5,6 +5,9 @@ source 'https://rubygems.org'
5
5
  # Specify your gem's dependencies in ey-hmac.gemspec
6
6
  gemspec
7
7
 
8
+ gem 'rubocop', require: false
9
+ gem 'rubocop-rspec', require: false
10
+
8
11
  group(:test) do
9
12
  gem 'pry-nav'
10
13
  gem 'rspec', '~> 3.3'
@@ -17,6 +20,6 @@ group(:rack) do
17
20
  end
18
21
 
19
22
  group(:faraday) do
20
- gem 'faraday', '~> 1.3'
23
+ gem 'faraday', '>= 1.3'
21
24
  gem 'faraday_middleware'
22
25
  end
data/Rakefile CHANGED
@@ -1 +1,3 @@
1
- require "bundler/gem_tasks"
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class Ey::Hmac::Adapter::Faraday < Ey::Hmac::Adapter
2
4
  def method
3
5
  request[:method].to_s.upcase
@@ -5,13 +7,13 @@ class Ey::Hmac::Adapter::Faraday < Ey::Hmac::Adapter
5
7
 
6
8
  def content_type
7
9
  @content_type ||= find_header(
8
- *%w[CONTENT-TYPE CONTENT_TYPE Content-Type Content_Type]
10
+ 'CONTENT-TYPE', 'CONTENT_TYPE', 'Content-Type', 'Content_Type'
9
11
  )
10
12
  end
11
13
 
12
14
  def content_digest
13
15
  @content_digest ||= find_header(
14
- *%w[CONTENT-DIGEST CONTENT_DIGEST Content-Digest Content_Digest]
16
+ 'CONTENT-DIGEST', 'CONTENT_DIGEST', 'Content-Digest', 'Content_Digest'
15
17
  )
16
18
  end
17
19
 
@@ -25,25 +27,21 @@ class Ey::Hmac::Adapter::Faraday < Ey::Hmac::Adapter
25
27
  body.to_s
26
28
  end
27
29
 
28
- if digestable && digestable != ""
30
+ if digestable && digestable != ''
29
31
  @content_digest = request[:request_headers]['Content-Digest'] = Digest::MD5.hexdigest(digestable)
30
32
  end
31
33
  end
32
34
 
33
35
  def body
34
- if request[:body] && request[:body].to_s != ""
35
- request[:body]
36
- end
36
+ request[:body] if request[:body] && request[:body].to_s != ''
37
37
  end
38
38
 
39
39
  def date
40
- find_header(*%w[DATE Date])
40
+ find_header('DATE', 'Date')
41
41
  end
42
42
 
43
43
  def set_date
44
- unless date
45
- request[:request_headers]['Date'] = Time.now.httpdate
46
- end
44
+ request[:request_headers]['Date'] = Time.now.httpdate unless date
47
45
  end
48
46
 
49
47
  def path
@@ -54,15 +52,13 @@ class Ey::Hmac::Adapter::Faraday < Ey::Hmac::Adapter
54
52
  set_content_digest
55
53
  set_date
56
54
 
57
- if options[:version]
58
- request[:request_headers]['X-Signature-Version'] = options[:version]
59
- end
55
+ request[:request_headers]['X-Signature-Version'] = options[:version] if options[:version]
60
56
 
61
57
  request[:request_headers][authorization_header] = authorization(key_id, key_secret)
62
58
  end
63
59
 
64
60
  def authorization_signature
65
- find_header(*%w[Authorization AUTHORIZATION])
61
+ find_header('Authorization', 'AUTHORIZATION')
66
62
  end
67
63
 
68
64
  private
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'rack'
2
4
 
3
5
  class Ey::Hmac::Adapter::Rack < Ey::Hmac::Adapter
@@ -19,18 +21,15 @@ class Ey::Hmac::Adapter::Rack < Ey::Hmac::Adapter
19
21
  end
20
22
 
21
23
  def set_content_digest
22
- if body
23
- request.env['HTTP_CONTENT_DIGEST'] = Digest::MD5.hexdigest(body)
24
- end
24
+ request.env['HTTP_CONTENT_DIGEST'] = Digest::MD5.hexdigest(body) if body
25
25
  end
26
26
 
27
27
  def body
28
- if request.env["rack.input"]
29
- request.env["rack.input"].rewind
30
- body = request.env["rack.input"].read
31
- request.env["rack.input"].rewind
32
- body == "" ? nil : body
33
- else nil
28
+ if request.env['rack.input']
29
+ request.env['rack.input'].rewind
30
+ body = request.env['rack.input'].read
31
+ request.env['rack.input'].rewind
32
+ body == '' ? nil : body
34
33
  end
35
34
  end
36
35
 
@@ -50,9 +49,7 @@ class Ey::Hmac::Adapter::Rack < Ey::Hmac::Adapter
50
49
  set_date
51
50
  set_content_digest
52
51
 
53
- if options[:version]
54
- request.env['HTTP_X_SIGNATURE_VERSION'] = options[:version]
55
- end
52
+ request.env['HTTP_X_SIGNATURE_VERSION'] = options[:version] if options[:version]
56
53
 
57
54
  request.env["HTTP_#{authorization_header.to_s.upcase}"] = authorization(key_id, key_secret)
58
55
  end
@@ -1,12 +1,22 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # This class is responsible for forming the canonical string to used to sign requests
2
4
  # @abstract override methods {#method}, {#path}, {#body}, {#content_type} and {#content_digest}
3
5
  class Ey::Hmac::Adapter
4
- AUTHORIZATION_REGEXP = /\w+ ([^:]+):(.+)$/
6
+ AUTHORIZATION_REGEXP = /\w+ ([^:]+):(.+)$/.freeze
7
+ DEFAULT_CANONICALIZE_WITH = %i[method content_type content_digest date path].freeze
5
8
 
6
- autoload :Rack, "ey-hmac/adapter/rack"
7
- autoload :Faraday, "ey-hmac/adapter/faraday"
9
+ autoload :Rack, 'ey-hmac/adapter/rack'
10
+ autoload :Faraday, 'ey-hmac/adapter/faraday'
8
11
 
9
- attr_reader :request, :options, :authorization_header, :service, :sign_with, :accept_digests
12
+ attr_reader :request,
13
+ :options,
14
+ :authorization_header,
15
+ :service,
16
+ :sign_with,
17
+ :accept_digests,
18
+ :include_query_string,
19
+ :canonicalize_with
10
20
 
11
21
  # @param [Object] request signer-specific request implementation
12
22
  # @option options [Integer] :version signature version
@@ -14,15 +24,21 @@ class Ey::Hmac::Adapter
14
24
  # @option options [String] :authorization_header ('Authorization') Authorization header key.
15
25
  # @option options [String] :server ('EyHmac') service name prefixed to {#authorization}. set to {#service}
16
26
  # @option options [Symbol] :sign_with (:sha_256) outgoing signature digest algorithm. See {OpenSSL::Digest#new}
27
+ # @option options [Symbol] :include_query_string (false) canonicalize with the request query string.
17
28
  # @option options [Array] :accepted_digests ([:sha_256]) accepted incoming signature digest algorithm. See {OpenSSL::Digest#new}
18
- def initialize(request, options={})
19
- @request, @options = request, options
29
+ def initialize(request, options = {})
30
+ @request = request
31
+ @options = options
20
32
 
21
33
  @ttl = options[:ttl]
22
34
  @authorization_header = options[:authorization_header] || 'Authorization'
23
35
  @service = options[:service] || 'EyHmac'
24
36
  @sign_with = options[:sign_with] || :sha256
25
- @accept_digests = Array(options[:accept_digests] || :sha256)
37
+ @include_query_string = options.fetch(:include_query_string, false)
38
+ @accept_digests = Array(options[:accept_digests] || :sha256)
39
+
40
+ @canonicalize_with = DEFAULT_CANONICALIZE_WITH
41
+ @canonicalize_with += :query_string if include_query_string
26
42
  end
27
43
 
28
44
  # In order for the server to correctly authorize the request, the client and server MUST AGREE on this format
@@ -30,16 +46,18 @@ class Ey::Hmac::Adapter
30
46
  # default canonical string formation is '{#method}\\n{#content_type}\\n{#content_digest}\\n{#date}\\n{#path}'
31
47
  # @return [String] canonical string used to form the {#signature}
32
48
  def canonicalize
33
- [method, content_type, content_digest, date, path].join("\n")
49
+ canonicalize_with.map { |message| public_send(message) }.join("\n")
34
50
  end
35
51
 
36
52
  # @param [String] key_secret private HMAC key
37
53
  # @param [String] signature digest hash function. Defaults to #sign_with
38
54
  # @return [String] HMAC signature of {#request}
39
- def signature(key_secret, digest = self.sign_with)
55
+ def signature(key_secret, digest = sign_with)
40
56
  Base64.strict_encode64(
41
57
  OpenSSL::HMAC.digest(
42
- OpenSSL::Digest.new(digest.to_s), key_secret, canonicalize)).strip
58
+ OpenSSL::Digest.new(digest.to_s), key_secret, canonicalize
59
+ )
60
+ ).strip
43
61
  end
44
62
 
45
63
  # @param [String] key_id public HMAC key
@@ -106,7 +124,7 @@ class Ey::Hmac::Adapter
106
124
  # @yieldparam key_id [String] public HMAC key
107
125
  # @return [Boolean] true if block yields matching private key and signature matches, else false
108
126
  # @see #authenticated!
109
- def authenticated?(options={}, &block)
127
+ def authenticated?(_options = {}, &block)
110
128
  authenticated!(&block)
111
129
  rescue Ey::Hmac::Error
112
130
  false
@@ -119,18 +137,18 @@ class Ey::Hmac::Adapter
119
137
 
120
138
  unless key_secret
121
139
  raise Ey::Hmac::MissingSecret,
122
- "Failed to find secret matching #{key_id.inspect}"
140
+ "Failed to find secret matching #{key_id.inspect}"
123
141
  end
124
142
 
125
143
  check_ttl!
126
144
 
127
- calculated_signatures = accept_digests.map { |ad| signature(key_secret, ad) }
128
- matching_signature = calculated_signatures.any? { |cs| secure_compare(signature_value, cs) }
145
+ matching_signature =
146
+ accept_digests
147
+ .lazy
148
+ .map { |ad| signature(key_secret, ad) }
149
+ .any? { |cs| secure_compare(signature_value, cs) }
129
150
 
130
- unless matching_signature
131
- raise Ey::Hmac::SignatureMismatch,
132
- "Calculated signature #{signature_value} does not match #{calculated_signatures.inspect} using #{canonicalize.inspect}"
133
- end
151
+ raise Ey::Hmac::SignatureMismatch unless matching_signature
134
152
 
135
153
  true
136
154
  end
@@ -143,11 +161,12 @@ class Ey::Hmac::Adapter
143
161
  def secure_compare(a, b)
144
162
  return false unless a.bytesize == b.bytesize
145
163
 
146
- l = a.unpack("C*")
164
+ l = a.unpack('C*')
147
165
 
148
- r, i = 0, -1
149
- b.each_byte { |v| r |= v ^ l[i+=1] }
150
- r == 0
166
+ r = 0
167
+ i = -1
168
+ b.each_byte { |v| r |= v ^ l[i += 1] }
169
+ r.zero?
151
170
  end
152
171
 
153
172
  def check_ttl!
@@ -157,7 +176,7 @@ class Ey::Hmac::Adapter
157
176
 
158
177
  unless expiry > current_time
159
178
  raise Ey::Hmac::ExpiredHmac,
160
- "Signature has expired passed #{expiry}. Current time is #{current_time}"
179
+ "Signature has expired passed #{expiry}. Current time is #{current_time}"
161
180
  end
162
181
  end
163
182
  end
@@ -167,7 +186,7 @@ class Ey::Hmac::Adapter
167
186
 
168
187
  unless authorization_match
169
188
  raise Ey::Hmac::MissingAuthorization,
170
- "Failed to parse authorization_signature #{authorization_signature}"
189
+ "Failed to parse authorization_signature #{authorization_signature}"
171
190
  end
172
191
 
173
192
  [authorization_match[1], authorization_match[2]]
@@ -1,21 +1,24 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'ey-hmac'
2
4
  require 'faraday'
3
5
 
4
- class Ey::Hmac::Faraday < Faraday::Response::Middleware
5
- dependency("ey-hmac")
6
+ class Ey::Hmac::Faraday < Faraday::Middleware
7
+ dependency('ey-hmac') if respond_to?(:dependency)
6
8
 
7
9
  attr_reader :key_id, :key_secret, :options
8
10
 
9
11
  def initialize(app, key_id, key_secret, options = {})
10
12
  super(app)
11
- @key_id, @key_secret = key_id, key_secret
13
+ @key_id = key_id
14
+ @key_secret = key_secret
12
15
  @options = options
13
16
  end
14
17
 
15
18
  def call(env)
16
- Ey::Hmac.sign!(env, key_id, key_secret, {adapter: Ey::Hmac::Adapter::Faraday}.merge(options))
19
+ Ey::Hmac.sign!(env, key_id, key_secret, { adapter: Ey::Hmac::Adapter::Faraday }.merge(options))
17
20
  @app.call(env)
18
21
  end
19
22
  end
20
23
 
21
- Faraday::Middleware.register_middleware :hmac => Ey::Hmac::Faraday
24
+ Faraday::Middleware.register_middleware hmac: Ey::Hmac::Faraday
data/lib/ey-hmac/rack.rb CHANGED
@@ -1,10 +1,13 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # Request middleware that performs HMAC request signing
2
4
  class Ey::Hmac::Rack
3
5
  attr_reader :key_id, :key_secret, :options
4
6
 
5
7
  def initialize(app, key_id, key_secret, options = {})
6
8
  @app = app
7
- @key_id, @key_secret = key_id, key_secret
9
+ @key_id = key_id
10
+ @key_secret = key_secret
8
11
  @options = options
9
12
  end
10
13
 
@@ -1,5 +1,7 @@
1
- module Ey
1
+ # frozen_string_literal: true
2
+
3
+ module Ey # rubocop:disable Style/ClassAndModuleChildren
2
4
  module Hmac
3
- VERSION = "2.3.0"
5
+ VERSION = '2.4.0'
4
6
  end
5
7
  end