oauth 0.4.7 → 0.5.4

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of oauth might be problematic. Click here for more details.

Files changed (78) hide show
  1. checksums.yaml +7 -0
  2. data/README.rdoc +21 -10
  3. data/bin/oauth +8 -2
  4. data/lib/oauth.rb +3 -5
  5. data/lib/oauth/cli.rb +37 -359
  6. data/lib/oauth/cli/authorize_command.rb +71 -0
  7. data/lib/oauth/cli/base_command.rb +208 -0
  8. data/lib/oauth/cli/help_command.rb +22 -0
  9. data/lib/oauth/cli/query_command.rb +25 -0
  10. data/lib/oauth/cli/sign_command.rb +81 -0
  11. data/lib/oauth/cli/version_command.rb +7 -0
  12. data/lib/oauth/client/action_controller_request.rb +1 -1
  13. data/lib/oauth/client/em_http.rb +0 -1
  14. data/lib/oauth/client/helper.rb +4 -0
  15. data/lib/oauth/client/net_http.rb +9 -8
  16. data/lib/oauth/consumer.rb +35 -8
  17. data/lib/oauth/helper.rb +11 -7
  18. data/lib/oauth/request_proxy/action_controller_request.rb +27 -3
  19. data/lib/oauth/request_proxy/action_dispatch_request.rb +7 -0
  20. data/lib/oauth/request_proxy/base.rb +7 -3
  21. data/lib/oauth/request_proxy/net_http.rb +1 -1
  22. data/lib/oauth/request_proxy/rest_client_request.rb +62 -0
  23. data/lib/oauth/request_proxy/typhoeus_request.rb +4 -3
  24. data/lib/oauth/signature/base.rb +9 -23
  25. data/lib/oauth/signature/hmac/sha1.rb +12 -4
  26. data/lib/oauth/signature/plaintext.rb +6 -0
  27. data/lib/oauth/signature/rsa/sha1.rb +7 -3
  28. data/lib/oauth/tokens/access_token.rb +12 -0
  29. data/lib/oauth/tokens/request_token.rb +6 -1
  30. data/lib/oauth/tokens/token.rb +1 -1
  31. data/lib/oauth/version.rb +3 -0
  32. metadata +147 -103
  33. data/.gemtest +0 -0
  34. data/Gemfile +0 -16
  35. data/Gemfile.lock +0 -47
  36. data/HISTORY +0 -173
  37. data/Rakefile +0 -37
  38. data/examples/yql.rb +0 -44
  39. data/lib/digest/hmac.rb +0 -104
  40. data/lib/oauth/core_ext.rb +0 -31
  41. data/lib/oauth/signature/hmac/base.rb +0 -15
  42. data/lib/oauth/signature/hmac/md5.rb +0 -8
  43. data/lib/oauth/signature/hmac/rmd160.rb +0 -8
  44. data/lib/oauth/signature/hmac/sha2.rb +0 -8
  45. data/lib/oauth/signature/md5.rb +0 -13
  46. data/lib/oauth/signature/sha1.rb +0 -13
  47. data/oauth.gemspec +0 -148
  48. data/tasks/deployment.rake +0 -34
  49. data/tasks/environment.rake +0 -7
  50. data/tasks/website.rake +0 -17
  51. data/test/cases/oauth_case.rb +0 -19
  52. data/test/cases/spec/1_0-final/test_construct_request_url.rb +0 -62
  53. data/test/cases/spec/1_0-final/test_normalize_request_parameters.rb +0 -88
  54. data/test/cases/spec/1_0-final/test_parameter_encodings.rb +0 -86
  55. data/test/cases/spec/1_0-final/test_signature_base_strings.rb +0 -77
  56. data/test/integration/consumer_test.rb +0 -307
  57. data/test/keys/rsa.cert +0 -11
  58. data/test/keys/rsa.pem +0 -16
  59. data/test/test_access_token.rb +0 -26
  60. data/test/test_action_controller_request_proxy.rb +0 -133
  61. data/test/test_consumer.rb +0 -220
  62. data/test/test_curb_request_proxy.rb +0 -77
  63. data/test/test_em_http_client.rb +0 -80
  64. data/test/test_em_http_request_proxy.rb +0 -115
  65. data/test/test_helper.rb +0 -28
  66. data/test/test_hmac_sha1.rb +0 -20
  67. data/test/test_net_http_client.rb +0 -292
  68. data/test/test_net_http_request_proxy.rb +0 -72
  69. data/test/test_oauth_helper.rb +0 -94
  70. data/test/test_rack_request_proxy.rb +0 -40
  71. data/test/test_request_token.rb +0 -51
  72. data/test/test_rsa_sha1.rb +0 -59
  73. data/test/test_server.rb +0 -40
  74. data/test/test_signature.rb +0 -22
  75. data/test/test_signature_base.rb +0 -32
  76. data/test/test_signature_plain_text.rb +0 -31
  77. data/test/test_token.rb +0 -14
  78. data/test/test_typhoeus_request_proxy.rb +0 -80
@@ -1,5 +1,4 @@
1
1
  require 'oauth/helper'
2
- require 'oauth/client/helper'
3
2
  require 'oauth/request_proxy/net_http'
4
3
 
5
4
  class Net::HTTPGenericRequest
@@ -21,7 +20,8 @@ class Net::HTTPGenericRequest
21
20
  # This method also modifies the <tt>User-Agent</tt> header to add the OAuth gem version.
22
21
  #
23
22
  # See Also: {OAuth core spec version 1.0, section 5.4.1}[http://oauth.net/core/1.0#rfc.section.5.4.1],
24
- # {OAuth Request Body Hash 1.0 Draft 4}[http://oauth.googlecode.com/svn/spec/ext/body_hash/1.0/drafts/4/spec.html]
23
+ # {OAuth Request Body Hash 1.0 Draft 4}[http://oauth.googlecode.com/svn/spec/ext/body_hash/1.0/drafts/4/spec.html,
24
+ # http://oauth.googlecode.com/svn/spec/ext/body_hash/1.0/oauth-bodyhash.html#when_to_include]
25
25
  def oauth!(http, consumer = nil, token = nil, options = {})
26
26
  helper_options = oauth_helper_options(http, consumer, token, options)
27
27
  @oauth_helper = OAuth::Client::Helper.new(self, helper_options)
@@ -42,13 +42,14 @@ class Net::HTTPGenericRequest
42
42
  # * options - Request-specific options (e.g. +request_uri+, +consumer+, +token+, +scheme+,
43
43
  # +signature_method+, +nonce+, +timestamp+)
44
44
  #
45
- # See Also: {OAuth core spec version 1.0, section 9.1.1}[http://oauth.net/core/1.0#rfc.section.9.1.1],
46
- # {OAuth Request Body Hash 1.0 Draft 4}[http://oauth.googlecode.com/svn/spec/ext/body_hash/1.0/drafts/4/spec.html]
45
+ # See Also: {OAuth core spec version 1.0, section 5.4.1}[http://oauth.net/core/1.0#rfc.section.5.4.1],
46
+ # {OAuth Request Body Hash 1.0 Draft 4}[http://oauth.googlecode.com/svn/spec/ext/body_hash/1.0/drafts/4/spec.html,
47
+ # http://oauth.googlecode.com/svn/spec/ext/body_hash/1.0/oauth-bodyhash.html#when_to_include]
47
48
  def signature_base_string(http, consumer = nil, token = nil, options = {})
48
49
  helper_options = oauth_helper_options(http, consumer, token, options)
49
- oauth_helper = OAuth::Client::Helper.new(self, helper_options)
50
- oauth_helper.hash_body if oauth_body_hash_required?
51
- oauth_helper.signature_base_string
50
+ @oauth_helper = OAuth::Client::Helper.new(self, helper_options)
51
+ @oauth_helper.hash_body if oauth_body_hash_required?
52
+ @oauth_helper.signature_base_string
52
53
  end
53
54
 
54
55
  private
@@ -84,7 +85,7 @@ private
84
85
  end
85
86
 
86
87
  def oauth_body_hash_required?
87
- request_body_permitted? && !content_type.to_s.downcase.start_with?("application/x-www-form-urlencoded")
88
+ !@oauth_helper.token_request? && request_body_permitted? && !content_type.to_s.downcase.start_with?("application/x-www-form-urlencoded")
88
89
  end
89
90
 
90
91
  def set_oauth_header
@@ -8,9 +8,9 @@ require 'cgi'
8
8
  module OAuth
9
9
  class Consumer
10
10
  # determine the certificate authority path to verify SSL certs
11
- CA_FILES = %w(/etc/ssl/certs/ca-certificates.crt /usr/share/curl/curl-ca-bundle.crt)
11
+ CA_FILES = %W(#{ENV['SSL_CERT_FILE']} /etc/ssl/certs/ca-certificates.crt /etc/pki/tls/certs/ca-bundle.crt /usr/share/curl/curl-ca-bundle.crt)
12
12
  CA_FILES.each do |ca_file|
13
- if File.exists?(ca_file)
13
+ if File.exist?(ca_file)
14
14
  CA_FILE = ca_file
15
15
  break
16
16
  end
@@ -43,6 +43,13 @@ module OAuth
43
43
  # Add a custom ca_file for consumer
44
44
  # :ca_file => '/etc/certs.pem'
45
45
 
46
+ # Possible values:
47
+ #
48
+ # nil, false - no debug output
49
+ # true - uses $stdout
50
+ # some_value - uses some_value
51
+ :debug_output => nil,
52
+
46
53
  :oauth_version => "1.0"
47
54
  }
48
55
 
@@ -87,6 +94,18 @@ module OAuth
87
94
  @http_method ||= @options[:http_method] || :post
88
95
  end
89
96
 
97
+ def debug_output
98
+ @debug_output ||= begin
99
+ case @options[:debug_output]
100
+ when nil, false
101
+ when true
102
+ $stdout
103
+ else
104
+ @options[:debug_output]
105
+ end
106
+ end
107
+ end
108
+
90
109
  # The HTTP object for the site. The HTTP Object is what you get when you do Net::HTTP.new
91
110
  def http
92
111
  @http ||= create_http
@@ -191,6 +210,7 @@ module OAuth
191
210
 
192
211
  # Creates a request and parses the result as url_encoded. This is used internally for the RequestToken and AccessToken requests.
193
212
  def token_request(http_method, path, token = nil, request_options = {}, *arguments)
213
+ request_options[:token_request] ||= true
194
214
  response = request(http_method, path, token, request_options, *arguments)
195
215
  case response.code.to_i
196
216
 
@@ -209,7 +229,7 @@ module OAuth
209
229
  end
210
230
  when (300..399)
211
231
  # this is a redirect
212
- uri = URI.parse(response.header['location'])
232
+ uri = URI.parse(response['location'])
213
233
  response.error! if uri.path == path # careful of those infinite redirects
214
234
  self.token_request(http_method, uri.path, token, request_options, arguments)
215
235
  when (400..499)
@@ -234,8 +254,8 @@ module OAuth
234
254
  end
235
255
 
236
256
  def request_endpoint
237
- return nil if @options[:request_endpoint].nil?
238
- @options[:request_endpoint].to_s
257
+ return nil if @options[:request_endpoint].nil?
258
+ @options[:request_endpoint].to_s
239
259
  end
240
260
 
241
261
  def scheme
@@ -320,6 +340,8 @@ module OAuth
320
340
 
321
341
  http_object.read_timeout = http_object.open_timeout = @options[:timeout] || 30
322
342
  http_object.open_timeout = @options[:open_timeout] if @options[:open_timeout]
343
+ http_object.ssl_version = @options[:ssl_version] if @options[:ssl_version]
344
+ http_object.set_debug_output(debug_output) if debug_output
323
345
 
324
346
  http_object
325
347
  end
@@ -328,13 +350,15 @@ module OAuth
328
350
  def create_http_request(http_method, path, *arguments)
329
351
  http_method = http_method.to_sym
330
352
 
331
- if [:post, :put].include?(http_method)
353
+ if [:post, :put, :patch].include?(http_method)
332
354
  data = arguments.shift
333
355
  end
334
356
 
335
357
  # if the base site contains a path, add it now
336
- uri = URI.parse(site)
337
- path = uri.path + path if uri.path && uri.path != '/'
358
+ # only add if the site host matches the current http object's host
359
+ # (in case we've specified a full url for token requests)
360
+ uri = URI.parse(site)
361
+ path = uri.path + path if uri.path && uri.path != '/' && uri.host == http.address
338
362
 
339
363
  headers = arguments.first.is_a?(Hash) ? arguments.shift : {}
340
364
 
@@ -345,6 +369,9 @@ module OAuth
345
369
  when :put
346
370
  request = Net::HTTP::Put.new(path,headers)
347
371
  request["Content-Length"] = '0' # Default to 0
372
+ when :patch
373
+ request = Net::HTTP::Patch.new(path,headers)
374
+ request["Content-Length"] = '0' # Default to 0
348
375
  when :get
349
376
  request = Net::HTTP::Get.new(path,headers)
350
377
  when :delete
@@ -9,9 +9,17 @@ module OAuth
9
9
  #
10
10
  # See Also: {OAuth core spec version 1.0, section 5.1}[http://oauth.net/core/1.0#rfc.section.5.1]
11
11
  def escape(value)
12
- URI::escape(value.to_s, OAuth::RESERVED_CHARACTERS)
12
+ _escape(value.to_s.to_str)
13
13
  rescue ArgumentError
14
- URI::escape(value.to_s.force_encoding(Encoding::UTF_8), OAuth::RESERVED_CHARACTERS)
14
+ _escape(value.to_s.to_str.force_encoding(Encoding::UTF_8))
15
+ end
16
+
17
+ def _escape(string)
18
+ URI::DEFAULT_PARSER.escape(string, OAuth::RESERVED_CHARACTERS)
19
+ end
20
+
21
+ def unescape(value)
22
+ URI::DEFAULT_PARSER.unescape(value.gsub('+', '%2B'))
15
23
  end
16
24
 
17
25
  # Generate a random key of up to +size+ bytes. The value returned is Base64 encoded with non-word
@@ -49,7 +57,7 @@ module OAuth
49
57
  end
50
58
  end * "&"
51
59
  end
52
-
60
+
53
61
  #Returns a string representation of the Hash like in URL query string
54
62
  # build_nested_query({:level_1 => {:level_2 => ['value_1','value_2']}}, 'prefix'))
55
63
  # #=> ["prefix%5Blevel_1%5D%5Blevel_2%5D%5B%5D=value_1", "prefix%5Blevel_1%5D%5Blevel_2%5D%5B%5D=value_2"]
@@ -94,10 +102,6 @@ module OAuth
94
102
  Hash[*params.flatten]
95
103
  end
96
104
 
97
- def unescape(value)
98
- URI.unescape(value.gsub('+', '%2B'))
99
- end
100
-
101
105
  def stringify_keys(hash)
102
106
  new_h = {}
103
107
  hash.each do |k, v|
@@ -1,11 +1,35 @@
1
1
  require 'active_support'
2
+ require "active_support/version"
2
3
  require 'action_controller'
3
- require 'action_controller/request'
4
4
  require 'uri'
5
5
 
6
+ if
7
+ Gem::Version.new(ActiveSupport::VERSION::STRING) < Gem::Version.new("3")
8
+ then # rails 2.x
9
+ require 'action_controller/request'
10
+ unless ActionController::Request::HTTP_METHODS.include?("patch")
11
+ ActionController::Request::HTTP_METHODS << "patch"
12
+ ActionController::Request::HTTP_METHOD_LOOKUP["PATCH"] = :patch
13
+ ActionController::Request::HTTP_METHOD_LOOKUP["patch"] = :patch
14
+ end
15
+
16
+ elsif
17
+ Gem::Version.new(ActiveSupport::VERSION::STRING) < Gem::Version.new("4")
18
+ then # rails 3.x
19
+ require 'action_dispatch/http/request'
20
+ unless ActionDispatch::Request::HTTP_METHODS.include?("patch")
21
+ ActionDispatch::Request::HTTP_METHODS << "patch"
22
+ ActionDispatch::Request::HTTP_METHOD_LOOKUP["PATCH"] = :patch
23
+ ActionDispatch::Request::HTTP_METHOD_LOOKUP["patch"] = :patch
24
+ end
25
+
26
+ else # rails 4.x and later - already has patch
27
+ require 'action_dispatch/http/request'
28
+ end
29
+
6
30
  module OAuth::RequestProxy
7
31
  class ActionControllerRequest < OAuth::RequestProxy::Base
8
- proxies(defined?(ActionController::AbstractRequest) ? ActionController::AbstractRequest : ActionController::Request)
32
+ proxies(defined?(ActionDispatch::AbstractRequest) ? ActionDispatch::AbstractRequest : ActionDispatch::Request)
9
33
 
10
34
  def method
11
35
  request.method.to_s.upcase
@@ -43,7 +67,7 @@ module OAuth::RequestProxy
43
67
 
44
68
  params.
45
69
  join('&').split('&').
46
- reject(&:blank?).
70
+ reject { |s| s.match(/\A\s*\z/) }.
47
71
  map { |p| p.split('=').map{|esc| CGI.unescape(esc)} }.
48
72
  reject { |kv| kv[0] == 'oauth_signature'}
49
73
  end
@@ -0,0 +1,7 @@
1
+ require 'oauth/request_proxy/rack_request'
2
+
3
+ module OAuth::RequestProxy
4
+ class ActionDispatchRequest < OAuth::RequestProxy::RackRequest
5
+ proxies ActionDispatch::Request
6
+ end
7
+ end
@@ -76,7 +76,7 @@ module OAuth::RequestProxy
76
76
  end
77
77
 
78
78
  def parameters_for_signature
79
- parameters.reject { |k,v| k == "oauth_signature" || unsigned_parameters.include?(k)}
79
+ parameters.select { |k,v| not signature_and_unsigned_parameters.include?(k) }
80
80
  end
81
81
 
82
82
  def oauth_parameters
@@ -87,6 +87,10 @@ module OAuth::RequestProxy
87
87
  parameters.reject { |k,v| OAuth::PARAMETERS.include?(k) }
88
88
  end
89
89
 
90
+ def signature_and_unsigned_parameters
91
+ unsigned_parameters+["oauth_signature"]
92
+ end
93
+
90
94
  # See 9.1.2 in specs
91
95
  def normalized_uri
92
96
  u = URI.parse(uri)
@@ -143,10 +147,10 @@ module OAuth::RequestProxy
143
147
  end
144
148
 
145
149
  def query_string_blank?
146
- if uri = request.request_uri
150
+ if uri = request.env['REQUEST_URI']
147
151
  uri.split('?', 2)[1].nil?
148
152
  else
149
- request.query_string.blank?
153
+ request.query_string.match(/\A\s*\z/)
150
154
  end
151
155
  end
152
156
 
@@ -66,7 +66,7 @@ module OAuth::RequestProxy::Net
66
66
 
67
67
  def auth_header_params
68
68
  return nil unless request['Authorization'] && request['Authorization'][0,5] == 'OAuth'
69
- auth_params = request['Authorization']
69
+ request['Authorization']
70
70
  end
71
71
  end
72
72
  end
@@ -0,0 +1,62 @@
1
+ require 'oauth/request_proxy/base'
2
+ require 'rest-client'
3
+ require 'uri'
4
+ require 'cgi'
5
+
6
+ module OAuth::RequestProxy::RestClient
7
+ class Request < OAuth::RequestProxy::Base
8
+ proxies RestClient::Request
9
+
10
+ def method
11
+ request.method.to_s.upcase
12
+ end
13
+
14
+ def uri
15
+ request.url
16
+ end
17
+
18
+ def parameters
19
+ if options[:clobber_request]
20
+ options[:parameters] || {}
21
+ else
22
+ post_parameters.merge(query_params).merge(options[:parameters] || {})
23
+ end
24
+ end
25
+
26
+ protected
27
+
28
+ def query_params
29
+ query = URI.parse(request.url).query
30
+ query ? CGI.parse(query) : {}
31
+ end
32
+
33
+ def request_params
34
+ end
35
+
36
+ def post_parameters
37
+ # Post params are only used if posting form data
38
+ if method == 'POST' || method == 'PUT'
39
+ OAuth::Helper.stringify_keys(query_string_to_hash(request.payload.to_s) || {})
40
+ else
41
+ {}
42
+ end
43
+ end
44
+
45
+ private
46
+
47
+ def query_string_to_hash(query)
48
+ keyvals = query.split('&').inject({}) do |result, q|
49
+ k,v = q.split('=')
50
+ if !v.nil?
51
+ result.merge({k => v})
52
+ elsif !result.key?(k)
53
+ result.merge({k => true})
54
+ else
55
+ result
56
+ end
57
+ end
58
+ keyvals
59
+ end
60
+
61
+ end
62
+ end
@@ -11,7 +11,7 @@ module OAuth::RequestProxy::Typhoeus
11
11
  # oauth_params = {:consumer => oauth_consumer, :token => access_token}
12
12
  # req = Typhoeus::Request.new(uri, options)
13
13
  # oauth_helper = OAuth::Client::Helper.new(req, oauth_params.merge(:request_uri => uri))
14
- # req.headers.merge!({"Authorization" => oauth_helper.header})
14
+ # req.options[:headers].merge!({"Authorization" => oauth_helper.header})
15
15
  # hydra = Typhoeus::Hydra.new()
16
16
  # hydra.queue(req)
17
17
  # hydra.run
@@ -19,7 +19,8 @@ module OAuth::RequestProxy::Typhoeus
19
19
  proxies Typhoeus::Request
20
20
 
21
21
  def method
22
- request.method.to_s.upcase
22
+ request_method = request.options[:method].to_s.upcase
23
+ request_method.empty? ? 'GET' : request_method
23
24
  end
24
25
 
25
26
  def uri
@@ -44,7 +45,7 @@ module OAuth::RequestProxy::Typhoeus
44
45
  def post_parameters
45
46
  # Post params are only used if posting form data
46
47
  if method == 'POST'
47
- OAuth::Helper.stringify_keys(request.params || {})
48
+ OAuth::Helper.stringify_keys(request.options[:params] || {})
48
49
  else
49
50
  {}
50
51
  end
@@ -16,21 +16,6 @@ module OAuth::Signature
16
16
  OAuth::Signature.available_methods[@implements] = self
17
17
  end
18
18
 
19
- def self.digest_class(digest_class = nil)
20
- return @digest_class if digest_class.nil?
21
- @digest_class = digest_class
22
- end
23
-
24
- def self.digest_klass(digest_klass = nil)
25
- return @digest_klass if digest_klass.nil?
26
- @digest_klass = digest_klass
27
- end
28
-
29
- def self.hash_class(hash_class = nil)
30
- return @hash_class if hash_class.nil?
31
- @hash_class = hash_class
32
- end
33
-
34
19
  def initialize(request, options = {}, &block)
35
20
  raise TypeError unless request.kind_of?(OAuth::RequestProxy::Base)
36
21
  @request = request
@@ -66,7 +51,7 @@ module OAuth::Signature
66
51
  end
67
52
 
68
53
  def ==(cmp_signature)
69
- Base64.decode64(signature) == Base64.decode64(cmp_signature)
54
+ signature == cmp_signature
70
55
  end
71
56
 
72
57
  def verify
@@ -78,14 +63,10 @@ module OAuth::Signature
78
63
  end
79
64
 
80
65
  def body_hash
81
- if self.class.hash_class
82
- Base64.encode64(self.class.hash_class.digest(request.body || '')).chomp.gsub(/\n/,'')
83
- else
84
- nil # no body hash algorithm defined, so don't generate one
85
- end
66
+ raise_instantiation_error
86
67
  end
87
68
 
88
- private
69
+ private
89
70
 
90
71
  def token
91
72
  request.token
@@ -104,7 +85,12 @@ module OAuth::Signature
104
85
  end
105
86
 
106
87
  def digest
107
- self.class.digest_class.digest(signature_base_string)
88
+ raise_instantiation_error
108
89
  end
90
+
91
+ def raise_instantiation_error
92
+ raise NotImplementedError, "Cannot instantiate #{self.class.name} class directly."
93
+ end
94
+
109
95
  end
110
96
  end
@@ -1,9 +1,17 @@
1
- require 'oauth/signature/hmac/base'
1
+ require 'oauth/signature/base'
2
2
 
3
3
  module OAuth::Signature::HMAC
4
- class SHA1 < Base
4
+ class SHA1 < OAuth::Signature::Base
5
5
  implements 'hmac-sha1'
6
- digest_klass 'SHA1'
7
- hash_class ::Digest::SHA1
6
+
7
+ def body_hash
8
+ Base64.encode64(OpenSSL::Digest::SHA1.digest(request.body || '')).chomp.gsub(/\n/,'')
9
+ end
10
+
11
+ private
12
+
13
+ def digest
14
+ OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha1'), secret, signature_base_string)
15
+ end
8
16
  end
9
17
  end