pelle-oauth 0.2.7 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. data/History.txt +14 -0
  2. data/Manifest.txt +19 -3
  3. data/README.rdoc +73 -0
  4. data/Rakefile +34 -4
  5. data/TODO +14 -0
  6. data/bin/oauth +5 -0
  7. data/lib/oauth.rb +3 -3
  8. data/lib/oauth/cli.rb +130 -0
  9. data/lib/oauth/client/helper.rb +3 -2
  10. data/lib/oauth/consumer.rb +1 -1
  11. data/lib/oauth/helper.rb +3 -0
  12. data/lib/oauth/oauth_test_helper.rb +26 -0
  13. data/lib/oauth/request_proxy/action_controller_request.rb +2 -4
  14. data/lib/oauth/request_proxy/base.rb +31 -0
  15. data/lib/oauth/request_proxy/jabber_request.rb +42 -0
  16. data/lib/oauth/request_proxy/mock_request.rb +36 -0
  17. data/lib/oauth/request_proxy/net_http.rb +0 -2
  18. data/lib/oauth/request_proxy/rack_request.rb +1 -3
  19. data/lib/oauth/signature/base.rb +20 -7
  20. data/lib/oauth/signature/plaintext.rb +1 -1
  21. data/lib/oauth/token.rb +3 -3
  22. data/lib/oauth/version.rb +2 -8
  23. data/oauth.gemspec +43 -0
  24. data/script/txt2html +1 -1
  25. data/test/cases/oauth_case.rb +19 -0
  26. data/test/cases/spec/1_0-final/test_construct_request_url.rb +62 -0
  27. data/test/cases/spec/1_0-final/test_normalize_request_parameters.rb +88 -0
  28. data/test/cases/spec/1_0-final/test_parameter_encodings.rb +86 -0
  29. data/test/cases/spec/1_0-final/test_signature_base_strings.rb +77 -0
  30. data/test/keys/rsa.cert +11 -0
  31. data/test/keys/rsa.pem +16 -0
  32. data/test/test_action_controller_request_proxy.rb +2 -1
  33. data/test/test_consumer.rb +17 -18
  34. data/test/test_helper.rb +8 -0
  35. data/test/test_net_http_client.rb +7 -7
  36. data/test/test_net_http_request_proxy.rb +3 -3
  37. data/test/test_rack_request_proxy.rb +3 -3
  38. data/test/test_rsa_sha1.rb +59 -0
  39. data/test/test_signature_plain_text.rb +31 -0
  40. data/website/index.html +1 -1
  41. metadata +53 -14
  42. data/config/hoe.rb +0 -71
  43. data/config/requirements.rb +0 -17
@@ -1,7 +1,10 @@
1
1
  require 'oauth/request_proxy'
2
+ require 'oauth/helper'
2
3
 
3
4
  module OAuth::RequestProxy
4
5
  class Base
6
+ include OAuth::Helper
7
+
5
8
  def self.proxies(klass)
6
9
  OAuth::RequestProxy.available_proxies[klass] = self
7
10
  end
@@ -47,6 +50,34 @@ module OAuth::RequestProxy
47
50
  parameters['oauth_signature'] || ""
48
51
  end
49
52
 
53
+ # See 9.1.2 in specs
54
+ def normalized_uri
55
+ u=URI.parse(uri)
56
+ "#{u.scheme.downcase}://#{u.host.downcase}#{(u.scheme.downcase=='http'&&u.port!=80)||(u.scheme.downcase=='https'&&u.port!=443) ? ":#{u.port}" : ""}#{(u.path&&u.path!='') ? u.path : '/'}"
57
+ end
58
+
59
+ # See 9.1.1. in specs Normalize Request Parameters
60
+ def normalized_parameters
61
+ parameters_for_signature.sort.map do |k, values|
62
+
63
+ if values.is_a?(Array)
64
+ # multiple values were provided for a single key
65
+ values.sort.collect do |v|
66
+ [escape(k),escape(v)] * "="
67
+ end
68
+ else
69
+ [escape(k),escape(values)] * "="
70
+ end
71
+ end * "&"
72
+ end
73
+
74
+ # See 9.1 in specs
75
+ def signature_base_string
76
+ base = [method, normalized_uri, normalized_parameters]
77
+ base.map { |v| escape(v) }.join("&")
78
+ end
79
+
80
+
50
81
  protected
51
82
 
52
83
  def header_params
@@ -0,0 +1,42 @@
1
+ require 'xmpp4r'
2
+ require 'oauth/request_proxy/base'
3
+
4
+ module OAuth
5
+ module RequestProxy
6
+ class JabberRequest < OAuth::RequestProxy::Base
7
+ proxies Jabber::Iq
8
+ proxies Jabber::Presence
9
+ proxies Jabber::Message
10
+
11
+ def parameters
12
+ return @params if @params
13
+
14
+ @params = {}
15
+
16
+ oauth = @request.get_elements('//oauth').first
17
+ return @params unless oauth
18
+
19
+ %w( oauth_token oauth_consumer_key oauth_signature_method oauth_signature
20
+ oauth_timestamp oauth_nonce oauth_version ).each do |param|
21
+ next unless element = oauth.first_element(param)
22
+ @params[param] = element.text
23
+ end
24
+
25
+ @params
26
+ end
27
+
28
+ def method
29
+ @request.name
30
+ end
31
+
32
+ def uri
33
+ [@request.from.strip.to_s, @request.to.strip.to_s].join("&")
34
+ end
35
+
36
+ def normalized_uri
37
+ uri
38
+ end
39
+
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,36 @@
1
+ require 'oauth/request_proxy/base'
2
+
3
+ module OAuth
4
+ module RequestProxy
5
+ # RequestProxy for Hashes to facilitate simpler signature creation.
6
+ # Usage:
7
+ # request = OAuth::RequestProxy.proxy \
8
+ # "method" => "iq",
9
+ # "uri" => [from, to] * "&",
10
+ # "parameters" => {
11
+ # "oauth_consumer_key" => oauth_consumer_key,
12
+ # "oauth_token" => oauth_token,
13
+ # "oauth_signature_method" => "HMAC-SHA1"
14
+ # }
15
+ #
16
+ # signature = OAuth::Signature.sign \
17
+ # request,
18
+ # :consumer_secret => oauth_consumer_secret,
19
+ # :token_secret => oauth_token_secret,
20
+ class MockRequest < OAuth::RequestProxy::Base
21
+ proxies Hash
22
+
23
+ def parameters
24
+ @request["parameters"]
25
+ end
26
+
27
+ def method
28
+ @request["method"]
29
+ end
30
+
31
+ def uri
32
+ @request["uri"]
33
+ end
34
+ end
35
+ end
36
+ end
@@ -14,8 +14,6 @@ module OAuth::RequestProxy::Net
14
14
 
15
15
  def uri
16
16
  uri = options[:uri]
17
- uri = URI.parse(uri) unless uri.kind_of?(URI)
18
- uri.query = nil
19
17
  uri.to_s
20
18
  end
21
19
 
@@ -11,9 +11,7 @@ module OAuth::RequestProxy
11
11
  end
12
12
 
13
13
  def uri
14
- uri = URI.parse(request.url)
15
- uri.query = nil
16
- uri.to_s
14
+ request.url
17
15
  end
18
16
 
19
17
  def parameters
@@ -24,11 +24,26 @@ module OAuth::Signature
24
24
  raise TypeError unless request.kind_of?(OAuth::RequestProxy::Base)
25
25
  @request = request
26
26
  @options = options
27
+
27
28
  if block_given?
28
- @token_secret, @consumer_secret = yield block.arity == 1 ? token : [token, consumer_key,nonce,request.timestamp]
29
+
30
+ # consumer secret and token secret need to be looked up based on pieces of the request
31
+ @token_secret, @consumer_secret = yield block.arity == 1 ? request : [token, consumer_key,nonce,request.timestamp]
32
+
29
33
  else
30
- @consumer_secret = @options[:consumer].secret
31
- @token_secret = @options[:token] ? @options[:token].secret : ''
34
+ ## consumer secret was determined beforehand
35
+
36
+ @consumer_secret = options[:consumer].secret if options[:consumer]
37
+
38
+ # presence of :consumer_secret option will override any Consumer that's provided
39
+ @consumer_secret = options[:consumer_secret] if options[:consumer_secret]
40
+
41
+ ## token secret was determined beforehand
42
+
43
+ @token_secret = options[:token].secret if options[:token]
44
+
45
+ # presence of :token_secret option will override any Token that's provided
46
+ @token_secret = options[:token_secret] if options[:token_secret]
32
47
  end
33
48
  end
34
49
 
@@ -45,11 +60,9 @@ module OAuth::Signature
45
60
  end
46
61
 
47
62
  def signature_base_string
48
- normalized_params = request.parameters_for_signature.sort.map { |k,v| [escape(k), escape(v)] * "=" }.join("&")
49
- base = [request.method, request.uri, normalized_params]
50
- sbs = base.map { |v| escape(v) }.join("&")
63
+ request.signature_base_string
51
64
  end
52
-
65
+
53
66
  private
54
67
 
55
68
  def token
@@ -9,7 +9,7 @@ module OAuth::Signature
9
9
  end
10
10
 
11
11
  def ==(cmp_signature)
12
- signature == cmp_signature
12
+ signature == escape(cmp_signature)
13
13
  end
14
14
 
15
15
  def signature_base_string
data/lib/oauth/token.rb CHANGED
@@ -30,7 +30,7 @@ module OAuth
30
30
  class ConsumerToken<Token
31
31
  attr_accessor :consumer
32
32
 
33
- def initialize(consumer,token,secret)
33
+ def initialize(consumer,token="",secret="")
34
34
  super token,secret
35
35
  @consumer=consumer
36
36
  end
@@ -62,7 +62,7 @@ module OAuth
62
62
 
63
63
  # exchange for AccessToken on server
64
64
  def get_access_token(options={})
65
- response=consumer.token_request(consumer.http_method,consumer.access_token_path,self,options)
65
+ response=consumer.token_request(consumer.http_method,consumer.access_token_url,self,options)
66
66
  OAuth::AccessToken.new(consumer,response[:oauth_token],response[:oauth_token_secret])
67
67
  end
68
68
  end
@@ -71,7 +71,7 @@ module OAuth
71
71
  class AccessToken<ConsumerToken
72
72
 
73
73
  # The less intrusive way. Otherwise, if we are to do it correctly inside consumer,
74
- # we need to restructure and touch more methods: requst(), sign!(), etc.
74
+ # we need to restructure and touch more methods: request(), sign!(), etc.
75
75
  def request(http_method, path, *arguments)
76
76
  request_uri = URI.parse(path)
77
77
  site_uri = consumer.uri
data/lib/oauth/version.rb CHANGED
@@ -1,9 +1,3 @@
1
- module Oauth #:nodoc:
2
- module VERSION #:nodoc:
3
- MAJOR = 0
4
- MINOR = 2
5
- TINY = 7
6
-
7
- STRING = [MAJOR, MINOR, TINY].join('.')
8
- end
1
+ module OAuth #:nodoc:
2
+ VERSION = '0.3.0'
9
3
  end
data/oauth.gemspec ADDED
@@ -0,0 +1,43 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = %q{oauth}
5
+ s.version = "0.3.0"
6
+
7
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
8
+ s.authors = ["Pelle Braendgaard", "Blaine Cook", "Larry Halff", "Jesse Clark", "Jon Crosby", "Seth Fitzsimmons"]
9
+ s.date = %q{2009-01-25}
10
+ s.default_executable = %q{oauth}
11
+ s.description = %q{OAuth Core Ruby implementation}
12
+ s.email = %q{pelleb@gmail.com}
13
+ s.executables = ["oauth"]
14
+ s.extra_rdoc_files = ["History.txt", "License.txt", "Manifest.txt", "README.rdoc", "specs.txt", "website/index.txt"]
15
+ s.files = ["History.txt", "License.txt", "Manifest.txt", "README.rdoc", "Rakefile", "TODO", "bin/oauth", "lib/oauth.rb", "lib/oauth/cli.rb", "lib/oauth/client.rb", "lib/oauth/client/action_controller_request.rb", "lib/oauth/client/helper.rb", "lib/oauth/client/net_http.rb", "lib/oauth/consumer.rb", "lib/oauth/helper.rb", "lib/oauth/oauth_test_helper.rb", "lib/oauth/request_proxy.rb", "lib/oauth/request_proxy/action_controller_request.rb", "lib/oauth/request_proxy/base.rb", "lib/oauth/request_proxy/jabber_request.rb", "lib/oauth/request_proxy/mock_request.rb", "lib/oauth/request_proxy/net_http.rb", "lib/oauth/request_proxy/rack_request.rb", "lib/oauth/server.rb", "lib/oauth/signature.rb", "lib/oauth/signature/base.rb", "lib/oauth/signature/hmac/base.rb", "lib/oauth/signature/hmac/md5.rb", "lib/oauth/signature/hmac/rmd160.rb", "lib/oauth/signature/hmac/sha1.rb", "lib/oauth/signature/hmac/sha2.rb", "lib/oauth/signature/md5.rb", "lib/oauth/signature/plaintext.rb", "lib/oauth/signature/rsa/sha1.rb", "lib/oauth/signature/sha1.rb", "lib/oauth/token.rb", "lib/oauth/version.rb", "oauth.gemspec", "script/destroy", "script/generate", "script/txt2html", "setup.rb", "specs.txt", "tasks/deployment.rake", "tasks/environment.rake", "tasks/website.rake", "test/cases/oauth_case.rb", "test/cases/spec/1_0-final/test_construct_request_url.rb", "test/cases/spec/1_0-final/test_normalize_request_parameters.rb", "test/cases/spec/1_0-final/test_parameter_encodings.rb", "test/cases/spec/1_0-final/test_signature_base_strings.rb", "test/keys/rsa.cert", "test/keys/rsa.pem", "test/test_action_controller_request_proxy.rb", "test/test_consumer.rb", "test/test_helper.rb", "test/test_hmac_sha1.rb", "test/test_net_http_client.rb", "test/test_net_http_request_proxy.rb", "test/test_rack_request_proxy.rb", "test/test_rsa_sha1.rb", "test/test_server.rb", "test/test_signature.rb", "test/test_signature_base.rb", "test/test_signature_plain_text.rb", "test/test_token.rb", "website/index.html", "website/index.txt", "website/javascripts/rounded_corners_lite.inc.js", "website/stylesheets/screen.css", "website/template.rhtml"]
16
+ s.has_rdoc = true
17
+ s.homepage = %q{http://oauth.rubyforge.org}
18
+ s.rdoc_options = ["--main", "README.rdoc"]
19
+ s.require_paths = ["lib"]
20
+ s.rubyforge_project = %q{oauth}
21
+ s.rubygems_version = %q{1.3.1}
22
+ s.summary = %q{OAuth Core Ruby implementation}
23
+ s.test_files = ["test/cases/spec/1_0-final/test_construct_request_url.rb", "test/cases/spec/1_0-final/test_normalize_request_parameters.rb", "test/cases/spec/1_0-final/test_parameter_encodings.rb", "test/cases/spec/1_0-final/test_signature_base_strings.rb", "test/test_action_controller_request_proxy.rb", "test/test_consumer.rb", "test/test_helper.rb", "test/test_hmac_sha1.rb", "test/test_net_http_client.rb", "test/test_net_http_request_proxy.rb", "test/test_rack_request_proxy.rb", "test/test_rsa_sha1.rb", "test/test_server.rb", "test/test_signature.rb", "test/test_signature_base.rb", "test/test_signature_plain_text.rb", "test/test_token.rb"]
24
+
25
+ if s.respond_to? :specification_version then
26
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
27
+ s.specification_version = 2
28
+
29
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
30
+ s.add_runtime_dependency(%q<ruby-hmac>, [">= 0.3.1"])
31
+ s.add_development_dependency(%q<newgem>, [">= 1.2.3"])
32
+ s.add_development_dependency(%q<hoe>, [">= 1.8.0"])
33
+ else
34
+ s.add_dependency(%q<ruby-hmac>, [">= 0.3.1"])
35
+ s.add_dependency(%q<newgem>, [">= 1.2.3"])
36
+ s.add_dependency(%q<hoe>, [">= 1.8.0"])
37
+ end
38
+ else
39
+ s.add_dependency(%q<ruby-hmac>, [">= 0.3.1"])
40
+ s.add_dependency(%q<newgem>, [">= 1.2.3"])
41
+ s.add_dependency(%q<hoe>, [">= 1.8.0"])
42
+ end
43
+ end
data/script/txt2html CHANGED
@@ -13,7 +13,7 @@ require 'syntax/convertors/html'
13
13
  require 'erb'
14
14
  require File.dirname(__FILE__) + '/../lib/oauth/version.rb'
15
15
 
16
- version = Oauth::VERSION::STRING
16
+ version = OAuth::VERSION::STRING
17
17
  download = 'http://rubyforge.org/projects/oauth'
18
18
 
19
19
  class Fixnum
@@ -0,0 +1,19 @@
1
+ require 'test/unit'
2
+ require 'oauth/signature'
3
+ require 'oauth/request_proxy/mock_request'
4
+
5
+
6
+ class OAuthCase < Test::Unit::TestCase
7
+ # avoid whining about a lack of tests
8
+ def run(*args)
9
+ return if @method_name.to_s == "default_test"
10
+ super
11
+ end
12
+
13
+ protected
14
+
15
+ # Creates a fake request
16
+ def request(params={},method='GET',uri="http://photos.example.net/photos")
17
+ OAuth::RequestProxy.proxy({'parameters'=>params,'method'=>method,'uri'=>uri})
18
+ end
19
+ end
@@ -0,0 +1,62 @@
1
+ require File.dirname(__FILE__) + '/../../oauth_case'
2
+
3
+ # See http://oauth.net/core/1.0/#anchor14
4
+ #
5
+ #9.1.2. Construct Request URL
6
+ #
7
+ #The Signature Base String includes the request absolute URL, tying the signature to a specific endpoint. The URL used in the Signature Base String MUST include the scheme, authority, and path, and MUST exclude the query and fragment as defined by [RFC3986] section 3.
8
+ #
9
+ #If the absolute request URL is not available to the Service Provider (it is always available to the Consumer), it can be constructed by combining the scheme being used, the HTTP Host header, and the relative HTTP request URL. If the Host header is not available, the Service Provider SHOULD use the host name communicated to the Consumer in the documentation or other means.
10
+ #
11
+ #The Service Provider SHOULD document the form of URL used in the Signature Base String to avoid ambiguity due to URL normalization. Unless specified, URL scheme and authority MUST be lowercase and include the port number; http default port 80 and https default port 443 MUST be excluded.
12
+ #
13
+ #For example, the request:
14
+ #
15
+ # HTTP://Example.com:80/resource?id=123
16
+ #Is included in the Signature Base String as:
17
+ #
18
+ # http://example.com/resource
19
+
20
+
21
+ class ConstructRequestUrlTest < OAuthCase
22
+
23
+ def test_from_spec
24
+ assert_request_url("http://example.com/resource","HTTP://Example.com:80/resource?id=123")
25
+ end
26
+
27
+ def test_simple_url_with_ending_slash
28
+ assert_request_url("http://example.com/","http://example.com/")
29
+ end
30
+
31
+ def test_simple_url_without_ending_slash
32
+ assert_request_url("http://example.com/","http://example.com")
33
+ end
34
+
35
+ def test_of_normalized_http
36
+ assert_request_url("http://example.com/resource","http://example.com/resource")
37
+ end
38
+
39
+ def test_of_https
40
+ assert_request_url("https://example.com/resource","HTTPS://Example.com:443/resource?id=123")
41
+ end
42
+
43
+ def test_of_normalized_http
44
+ assert_request_url("https://example.com/resource","https://example.com/resource")
45
+ end
46
+
47
+ def test_of_http_with_non_standard_port
48
+ assert_request_url("http://example.com:8080/resource","http://example.com:8080/resource")
49
+ end
50
+
51
+ def test_of_https_with_non_standard_port
52
+ assert_request_url("https://example.com:8080/resource","https://example.com:8080/resource")
53
+ end
54
+
55
+ protected
56
+
57
+
58
+ def assert_request_url(expected,given,message=nil)
59
+ assert_equal expected, request({},'GET',given).normalized_uri, message
60
+ end
61
+
62
+ end
@@ -0,0 +1,88 @@
1
+ require File.dirname(__FILE__) + '/../../oauth_case'
2
+
3
+ # See http://oauth.net/core/1.0/#anchor14
4
+ #
5
+ # 9.1.1. Normalize Request Parameters
6
+ #
7
+ # The request parameters are collected, sorted and concatenated into a normalized string:
8
+ #
9
+ # Parameters in the OAuth HTTP Authorization header excluding the realm parameter.
10
+ # Parameters in the HTTP POST request body (with a content-type of application/x-www-form-urlencoded).
11
+ # HTTP GET parameters added to the URLs in the query part (as defined by [RFC3986] section 3).
12
+ # The oauth_signature parameter MUST be excluded.
13
+ #
14
+ # The parameters are normalized into a single string as follows:
15
+ #
16
+ # Parameters are sorted by name, using lexicographical byte value ordering.
17
+ # If two or more parameters share the same name, they are sorted by their value. For example:
18
+ #
19
+ # a=1, c=hi%20there, f=25, f=50, f=a, z=p, z=t
20
+ # Parameters are concatenated in their sorted order into a single string. For each parameter,
21
+ # the name is separated from the corresponding value by an ‘=’ character (ASCII code 61), even
22
+ # if the value is empty. Each name-value pair is separated by an ‘&’ character (ASCII code 38). For example:
23
+ # a=1&c=hi%20there&f=25&f=50&f=a&z=p&z=t
24
+ #
25
+
26
+
27
+ class NormalizeRequestParametersTest < OAuthCase
28
+
29
+ def test_parameters_for_signature
30
+ params={'a'=>1, 'c'=>'hi there', 'f'=>'25', 'f'=>'50', 'f'=>'a', 'z'=>'p', 'z'=>'t'}
31
+ assert_equal params,request(params).parameters_for_signature
32
+ end
33
+
34
+
35
+ def test_parameters_for_signature_removes_oauth_signature
36
+ params={'a'=>1, 'c'=>'hi there', 'f'=>'25', 'f'=>'50', 'f'=>'a', 'z'=>'p', 'z'=>'t'}
37
+ assert_equal params,request(params.merge({'oauth_signature'=>'blalbla'})).parameters_for_signature
38
+ end
39
+
40
+ def test_spec_example
41
+ assert_normalized 'a=1&c=hi%20there&f=25&f=50&f=a&z=p&z=t', { 'a' => 1, 'c' => 'hi there', 'f' => ['25', '50', 'a'], 'z' => ['p', 't'] }
42
+ end
43
+
44
+ def test_sorts_parameters_correctly
45
+ # values for 'f' are scrambled
46
+ assert_normalized 'a=1&c=hi%20there&f=5&f=70&f=a&z=p&z=t', { 'a' => 1, 'c' => 'hi there', 'f' => ['a', '70', '5'], 'z' => ['p', 't'] }
47
+ end
48
+
49
+ def test_empty
50
+ assert_normalized "",{}
51
+ end
52
+
53
+
54
+ # These are from the wiki http://wiki.oauth.net/TestCases
55
+ # in the section Normalize Request Parameters
56
+ # Parameters have already been x-www-form-urlencoded (i.e. + = <space>)
57
+ def test_wiki1
58
+ assert_normalized "name=",{"name"=>nil}
59
+ end
60
+
61
+ def test_wiki2
62
+ assert_normalized "a=b",{'a'=>'b'}
63
+ end
64
+
65
+ def test_wiki3
66
+ assert_normalized "a=b&c=d",{'a'=>'b','c'=>'d'}
67
+ end
68
+
69
+ def test_wiki4
70
+ assert_normalized "a=x%20y&a=x%21y",{'a'=>["x!y","x y"]}
71
+
72
+ end
73
+
74
+ def test_wiki5
75
+ assert_normalized "x=a&x%21y=a",{"x!y"=>'a','x'=>'a'}
76
+ end
77
+
78
+ protected
79
+
80
+
81
+ def assert_normalized(expected,params,message=nil)
82
+ assert_equal expected, normalize_request_parameters(params), message
83
+ end
84
+
85
+ def normalize_request_parameters(params={})
86
+ request(params).normalized_parameters
87
+ end
88
+ end