kennedy 0.0.1

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.
Files changed (49) hide show
  1. data/.document +5 -0
  2. data/.gitignore +21 -0
  3. data/.yardoc +0 -0
  4. data/LICENSE +20 -0
  5. data/MAIN.rdoc +23 -0
  6. data/README.markdown +17 -0
  7. data/Rakefile +52 -0
  8. data/VERSION +1 -0
  9. data/bin/kennedy-gen +13 -0
  10. data/doc/Kennedy.html +98 -0
  11. data/doc/Kennedy/Backends.html +94 -0
  12. data/doc/Kennedy/Backends/LDAP.html +471 -0
  13. data/doc/Kennedy/BadTicketException.html +92 -0
  14. data/doc/Kennedy/Granter.html +570 -0
  15. data/doc/Kennedy/Server.html +258 -0
  16. data/doc/Kennedy/Ticket.html +875 -0
  17. data/doc/_index.html +170 -0
  18. data/doc/class_list.html +97 -0
  19. data/doc/css/common.css +1 -0
  20. data/doc/css/full_list.css +23 -0
  21. data/doc/css/style.css +261 -0
  22. data/doc/file.README.html +69 -0
  23. data/doc/file_list.html +29 -0
  24. data/doc/index.html +69 -0
  25. data/doc/js/app.js +91 -0
  26. data/doc/js/full_list.js +39 -0
  27. data/doc/js/jquery.js +19 -0
  28. data/doc/method_list.html +152 -0
  29. data/doc/top-level-namespace.html +80 -0
  30. data/kennedy.gemspec +114 -0
  31. data/lib/kennedy.rb +2 -0
  32. data/lib/kennedy/backends/ldap.rb +35 -0
  33. data/lib/kennedy/generator.rb +69 -0
  34. data/lib/kennedy/granter.rb +52 -0
  35. data/lib/kennedy/instance_configuration.rb +58 -0
  36. data/lib/kennedy/server.rb +164 -0
  37. data/lib/kennedy/ticket.rb +97 -0
  38. data/logo.png +0 -0
  39. data/template/config.ru.erb +22 -0
  40. data/template/config/api_keys.yml.erb +1 -0
  41. data/template/config/backend.rb +0 -0
  42. data/template/config/encryption.yml.erb +2 -0
  43. data/template/config/sessions.yml.erb +1 -0
  44. data/test/granter_test.rb +93 -0
  45. data/test/ldap_backend_test.rb +66 -0
  46. data/test/server_test.rb +285 -0
  47. data/test/teststrap.rb +34 -0
  48. data/test/ticket_test.rb +84 -0
  49. metadata +177 -0
@@ -0,0 +1,80 @@
1
+ <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
2
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
3
+ <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
4
+ <head>
5
+ <meta name="Content-Type" content="text/html; charset=UTF-8" />
6
+ <title>Top Level Namespace</title>
7
+ <link rel="stylesheet" href="css/style.css" type="text/css" media="screen" charset="utf-8" />
8
+ <link rel="stylesheet" href="css/common.css" type="text/css" media="screen" charset="utf-8" />
9
+
10
+ <script type="text/javascript" charset="utf-8">
11
+ relpath = '';
12
+ if (relpath != '') relpath += '/';
13
+ </script>
14
+ <script type="text/javascript" charset="utf-8" src="js/jquery.js"></script>
15
+ <script type="text/javascript" charset="utf-8" src="js/app.js"></script>
16
+
17
+ </head>
18
+ <body>
19
+ <div id="header">
20
+ <div id="menu">
21
+
22
+ <a href="_index.html">Index</a> &raquo;
23
+
24
+
25
+ <span class="title">Top Level Namespace</span>
26
+
27
+ </div>
28
+
29
+ <div id="search">
30
+ <a id="class_list_link" href="#">Namespace List</a>
31
+ <a id="method_list_link" href="#">Method List</a>
32
+ <a id ="file_list_link" href="#">File List</a>
33
+ </div>
34
+
35
+ <div class="clear"></div>
36
+ </div>
37
+
38
+ <iframe id="search_frame"></iframe>
39
+
40
+ <div id="content"><h1>Top Level Namespace
41
+
42
+
43
+ </h1>
44
+
45
+ <dl class="box">
46
+
47
+
48
+
49
+
50
+
51
+
52
+
53
+ </dl>
54
+ <div class="clear"></div>
55
+
56
+ <h2>Defined Under Namespace</h2>
57
+ <p class="children">
58
+
59
+
60
+ <strong class="modules">Modules:</strong> <a title="Kennedy" href="Kennedy.html">Kennedy</a>
61
+
62
+
63
+
64
+
65
+ </p>
66
+
67
+
68
+
69
+
70
+
71
+ </div>
72
+
73
+ <div id="footer">
74
+ Generated on Tue Dec 8 16:39:39 2009 by
75
+ <a href="http://yardoc.org" title="Yay! A Ruby Documentation Tool">yard</a>
76
+ 0.4.0 (ruby-1.8.7).
77
+ </div>
78
+
79
+ </body>
80
+ </html>
@@ -0,0 +1,114 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{kennedy}
8
+ s.version = "0.0.1"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["gabrielg"]
12
+ s.date = %q{2009-12-09}
13
+ s.description = %q{Kennedy is out for Castronaut. A simple single-sign-on client and server library.}
14
+ s.email = %q{gabriel.gironda@gmail.com}
15
+ s.executables = ["kennedy-gen", "kennedy-gen"]
16
+ s.extra_rdoc_files = [
17
+ "LICENSE",
18
+ "README.markdown"
19
+ ]
20
+ s.files = [
21
+ ".document",
22
+ ".gitignore",
23
+ ".yardoc",
24
+ "LICENSE",
25
+ "MAIN.rdoc",
26
+ "README.markdown",
27
+ "Rakefile",
28
+ "VERSION",
29
+ "bin/kennedy-gen",
30
+ "doc/Kennedy.html",
31
+ "doc/Kennedy/Backends.html",
32
+ "doc/Kennedy/Backends/LDAP.html",
33
+ "doc/Kennedy/BadTicketException.html",
34
+ "doc/Kennedy/Granter.html",
35
+ "doc/Kennedy/Server.html",
36
+ "doc/Kennedy/Ticket.html",
37
+ "doc/_index.html",
38
+ "doc/class_list.html",
39
+ "doc/css/common.css",
40
+ "doc/css/full_list.css",
41
+ "doc/css/style.css",
42
+ "doc/file.README.html",
43
+ "doc/file_list.html",
44
+ "doc/index.html",
45
+ "doc/js/app.js",
46
+ "doc/js/full_list.js",
47
+ "doc/js/jquery.js",
48
+ "doc/method_list.html",
49
+ "doc/top-level-namespace.html",
50
+ "kennedy.gemspec",
51
+ "lib/kennedy.rb",
52
+ "lib/kennedy/backends/ldap.rb",
53
+ "lib/kennedy/generator.rb",
54
+ "lib/kennedy/granter.rb",
55
+ "lib/kennedy/instance_configuration.rb",
56
+ "lib/kennedy/server.rb",
57
+ "lib/kennedy/ticket.rb",
58
+ "logo.png",
59
+ "template/config.ru.erb",
60
+ "template/config/api_keys.yml.erb",
61
+ "template/config/backend.rb",
62
+ "template/config/encryption.yml.erb",
63
+ "template/config/sessions.yml.erb",
64
+ "test/granter_test.rb",
65
+ "test/ldap_backend_test.rb",
66
+ "test/server_test.rb",
67
+ "test/teststrap.rb",
68
+ "test/ticket_test.rb"
69
+ ]
70
+ s.homepage = %q{http://github.com/gabrielg/kennedy}
71
+ s.rdoc_options = ["--charset=UTF-8"]
72
+ s.require_paths = ["lib"]
73
+ s.rubygems_version = %q{1.3.5}
74
+ s.summary = %q{A simple single-sign-on client and server library.}
75
+ s.test_files = [
76
+ "test/granter_test.rb",
77
+ "test/ldap_backend_test.rb",
78
+ "test/server_test.rb",
79
+ "test/teststrap.rb",
80
+ "test/ticket_test.rb"
81
+ ]
82
+
83
+ if s.respond_to? :specification_version then
84
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
85
+ s.specification_version = 3
86
+
87
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
88
+ s.add_development_dependency(%q<riot>, [">= 0"])
89
+ s.add_development_dependency(%q<maruku>, [">= 0"])
90
+ s.add_development_dependency(%q<yard>, [">= 0"])
91
+ s.add_runtime_dependency(%q<ruby-net-ldap>, [">= 0"])
92
+ s.add_runtime_dependency(%q<json>, [">= 0"])
93
+ s.add_runtime_dependency(%q<sinatra>, [">= 0"])
94
+ s.add_runtime_dependency(%q<rack>, [">= 0"])
95
+ else
96
+ s.add_dependency(%q<riot>, [">= 0"])
97
+ s.add_dependency(%q<maruku>, [">= 0"])
98
+ s.add_dependency(%q<yard>, [">= 0"])
99
+ s.add_dependency(%q<ruby-net-ldap>, [">= 0"])
100
+ s.add_dependency(%q<json>, [">= 0"])
101
+ s.add_dependency(%q<sinatra>, [">= 0"])
102
+ s.add_dependency(%q<rack>, [">= 0"])
103
+ end
104
+ else
105
+ s.add_dependency(%q<riot>, [">= 0"])
106
+ s.add_dependency(%q<maruku>, [">= 0"])
107
+ s.add_dependency(%q<yard>, [">= 0"])
108
+ s.add_dependency(%q<ruby-net-ldap>, [">= 0"])
109
+ s.add_dependency(%q<json>, [">= 0"])
110
+ s.add_dependency(%q<sinatra>, [">= 0"])
111
+ s.add_dependency(%q<rack>, [">= 0"])
112
+ end
113
+ end
114
+
@@ -0,0 +1,2 @@
1
+ require 'kennedy/granter'
2
+ require 'kennedy/backends/ldap'
@@ -0,0 +1,35 @@
1
+ module Kennedy
2
+ module Backends
3
+ class LDAP
4
+ attr_writer :filter
5
+
6
+ # Creates a new LDAP auth backend with the given arguments
7
+ # @param [Hash] args The arguments to construct the backend with
8
+ # @option args [String] :host The LDAP server host to connect to
9
+ # @option args [Hash] :auth The auth method ruby-net-ldap should use
10
+ # @option args [String] :base The treebase to check against
11
+ def initialize(args = {})
12
+ @host = args[:host] || raise(ArgumentError, "Host must be given as :host")
13
+ @auth = args[:auth] || raise(ArgumentError, "Auth must be given as :auth")
14
+ @base = args[:base] || raise(ArgumentError, "Base must be given as :base")
15
+ @filter = lambda { raise(ArgumentError, "Set a filter block on this object using the 'filter' writer") }
16
+ end
17
+
18
+ # Authenticates the given credentials against LDAP
19
+ # @param [String] identifier The identifier to filter on
20
+ # @param [String] password The password to use
21
+ # @return [true, false] A boolean indicating authentication success
22
+ def authenticate(identifier, password)
23
+ filter_string = @filter.call(identifier)
24
+ !!ldap_conn.bind_as(:filter => filter_string, :password => password)
25
+ end
26
+
27
+ private
28
+
29
+ def ldap_conn
30
+ @ldap_conn ||= Net::LDAP.new(:host => @host, :auth => @auth, :base => @base)
31
+ end
32
+
33
+ end # LDAP
34
+ end # Backends
35
+ end # Kennedy
@@ -0,0 +1,69 @@
1
+ require 'pathname'
2
+ require 'erb'
3
+ require 'fileutils'
4
+ require 'digest/sha1'
5
+
6
+ module Kennedy
7
+ class Generator
8
+ TemplateDir = (Pathname(__FILE__).parent.parent.parent + "template").expand_path
9
+ TemplateType = ".erb"
10
+
11
+ def run(arguments)
12
+ @app_name = arguments.first
13
+ raise ArgumentError, "An app name must be given" if @app_name.nil? || @app_name.empty?
14
+ create_destination_directory
15
+ copy_files
16
+ end
17
+
18
+ private
19
+
20
+ def create_destination_directory
21
+ @dest = Pathname(@app_name)
22
+ log_create(@dest)
23
+ @dest.mkpath
24
+ end
25
+
26
+ def copy_files
27
+ Pathname.glob("#{template_dir}/**/*").each do |pn|
28
+ next if pn.directory?
29
+ relative = pn.relative_path_from(TemplateDir)
30
+ dest_path = @dest + relative
31
+ unless dest_path.dirname.exist?
32
+ log_create(dest_path.dirname)
33
+ dest_path.dirname.mkpath
34
+ end
35
+ is_template?(pn) ? evaluate_and_write_template(pn, dest_path) : copy_file(pn, dest_path)
36
+ end
37
+ end
38
+
39
+ def is_template?(path)
40
+ Pathname(path).extname == template_type
41
+ end
42
+
43
+ def evaluate_and_write_template(path, dest)
44
+ dest = dest.sub(/#{Regexp.escape(dest.extname)}$/, "")
45
+ log_create(dest)
46
+ dest.open('w') do |f|
47
+ f << ERB.new(path.read).result(binding)
48
+ end
49
+ end
50
+
51
+ def copy_file(path, dest)
52
+ log_create(dest)
53
+ FileUtils.cp_r(path.expand_path.to_s, dest.expand_path.to_s)
54
+ end
55
+
56
+ def template_type
57
+ TemplateType
58
+ end
59
+
60
+ def template_dir
61
+ TemplateDir
62
+ end
63
+
64
+ def log_create(path)
65
+ puts "Creating '#{path}'"
66
+ end
67
+
68
+ end
69
+ end
@@ -0,0 +1,52 @@
1
+ require 'kennedy/ticket'
2
+
3
+ module Kennedy
4
+ # Granter is used to authenticate credentials and grant tickets to services
5
+ # once a client has been authenticated.
6
+ class Granter
7
+
8
+ # @param [Hash] args The arguments to create the granter with
9
+ # @option args [String] :iv The AES-256 initialization vector to use for encryption and decryption
10
+ # @option args [String] :passphrase The AES-256 passphrase to use for encryption and decryption
11
+ # @option args [Object] :backend An instance of a backend to use for authentication
12
+ def initialize(args = {})
13
+ @iv = args[:iv] || raise(ArgumentError, "Encryption IV must be given as :iv")
14
+ @passphrase = args[:passphrase] || raise(ArgumentError, "Encryption passphrase must be given as :passphrase")
15
+ @backend = args[:backend] || raise(ArgumentError, "Authentication backend must be given as :backend")
16
+ end
17
+
18
+ # Authenticates the given credentials against the current backend
19
+ # @param [Hash] args The arguments to authenticate with
20
+ # @option args [String] :identifier The identifier (email address, for example) to use for authentication
21
+ # @option args [String] :password The password to use for authentication
22
+ # @return [true, false] A boolean indication of whether authentication was successful or not
23
+ def authenticate(args = {})
24
+ !!@backend.authenticate(args[:identifier], args[:password])
25
+ end
26
+
27
+ # Generates a ticket object to pass back to clients requesting authentication
28
+ # @param [Hash] args The arguments to generate the ticket with
29
+ # @option args [String] :identifier The identifier (email address, for example) the ticket grants access for
30
+ # @return [Kennedy::Ticket] A Kennedy::Ticket object
31
+ def generate_ticket(args = {})
32
+ identifier = args[:identifier] || raise(ArgumentError, "An identifier must be given as :identifier")
33
+ new_ticket(identifier)
34
+ end
35
+
36
+ def read_ticket(args = {})
37
+ data = args[:data] || raise(ArgumentError, "Data must be given as :data")
38
+ decrypt_ticket(args[:data])
39
+ end
40
+
41
+ private
42
+
43
+ def decrypt_ticket(data)
44
+ Kennedy::Ticket.from_encrypted(:data => data, :iv => @iv, :passphrase => @passphrase)
45
+ end
46
+
47
+ def new_ticket(identifier)
48
+ Kennedy::Ticket.create(:identifier => identifier, :iv => @iv, :passphrase => @passphrase)
49
+ end
50
+
51
+ end # Granter
52
+ end
@@ -0,0 +1,58 @@
1
+ require 'yaml'
2
+ require 'pathname'
3
+
4
+ module Kennedy
5
+ class InstanceConfiguration
6
+ attr_reader :backend, :encryption, :api_keys
7
+
8
+ RequiredFiles = %w[backend.rb
9
+ encryption.yml
10
+ sessions.yml
11
+ api_keys.yml]
12
+
13
+ class << self
14
+ private :new
15
+ end
16
+
17
+ def initialize(config_dir)
18
+ @config_dir = config_dir
19
+ load_backend
20
+ load_encryption
21
+ load_sessions
22
+ load_api_keys
23
+ end
24
+
25
+ def self.load_config(config_dir)
26
+ config_dir = Pathname(config_dir)
27
+ raise ArgumentError, "Config dir '#{config_dir}' does not exist" unless config_dir.exist?
28
+ RequiredFiles.each do |rf|
29
+ expected = config_dir + rf
30
+ raise ArgumentError, "Expected config file '#{expected}' to exist" unless expected.exist?
31
+ end
32
+ new(config_dir)
33
+ end
34
+
35
+ def session_secret
36
+ @sessions['secret']
37
+ end
38
+
39
+ private
40
+
41
+ def load_backend
42
+ @backend = eval(File.read(@config_dir + "backend.rb"))
43
+ end
44
+
45
+ def load_encryption
46
+ @encryption = YAML.load_file(@config_dir + "encryption.yml")
47
+ end
48
+
49
+ def load_sessions
50
+ @sessions = YAML.load_file(@config_dir + "sessions.yml")
51
+ end
52
+
53
+ def load_api_keys
54
+ @api_keys = YAML.load_file(@config_dir + "api_keys.yml")
55
+ end
56
+
57
+ end # InstanceConfiguration
58
+ end # Kennedy
@@ -0,0 +1,164 @@
1
+ require 'sinatra/base'
2
+ require 'json'
3
+ require 'base64'
4
+ require 'rack/session/cookie'
5
+ require 'kennedy'
6
+
7
+ module Kennedy
8
+ class Server < Sinatra::Base
9
+ disable :session
10
+
11
+ # Creates a new subclass of Kennedy::Server with the given options
12
+ # @param [Hash] opts The options to use when building the subclass
13
+ # @option opts [Hash] :encryption The IV and passphrase to use when generating tickets, given as
14
+ # :iv and :passphrase keys in a Hash.
15
+ # @option opts [Object] :backend An instance of a backend to use for authentication.
16
+ # @option opts [String] :session_secret A secret for Rack::Session::Cookie to use when generating session
17
+ # cookies.
18
+ # @option opts [Hash] :api_keys A hash of key-value pairs to use as API users/keys with HTTP basic
19
+ # authentication
20
+ #
21
+ def self.create(opts = {})
22
+ sc = Class.new(self)
23
+ sc.instance_eval do
24
+ opts.each { |k,v| set k.to_sym, v }
25
+ raise ArgumentError, "A session secret must be set with :session_secret" unless defined?(session_secret)
26
+ add_cookie_middleware
27
+ set :api_keys, (defined?(api_keys) ? api_keys : {})
28
+ set :require_ssl, (defined?(require_ssl) ? require_ssl : true)
29
+ end
30
+ end
31
+
32
+ # Ensures all connections come in over SSL
33
+ before do
34
+ next unless require_ssl?
35
+ unless (request.env['HTTP_X_FORWARDED_PROTO'] || request.env['rack.url_scheme']) == 'https'
36
+ halt 403, "Only SSL connections are accepted."
37
+ end
38
+ end
39
+
40
+ # Ensures all connections come in requesting JSON
41
+ before do
42
+ unless request.content_type == 'application/json'
43
+ halt 415, "Only JSON requests are accepted."
44
+ end
45
+ end
46
+
47
+ # Parses request body as JSON
48
+ before do
49
+ begin
50
+ @json = JSON.parse(request.body.read)
51
+ rescue
52
+ @json = {}
53
+ end
54
+ end
55
+
56
+ # Takes incoming requests with a 'ticket' property in the JSON body and decrypts the ticket,
57
+ # returning an identifier as the 'identifier' property in the JSON response body if the
58
+ # ticket is valid and unexpired.
59
+ post "/validation_request" do
60
+ content_type "application/json"
61
+ require_api_authentication
62
+ begin
63
+ encrypted_ticket = Base64.decode64(@json['ticket'])
64
+ ticket = granter.read_ticket(:data => encrypted_ticket)
65
+ if ticket.expired?
66
+ [406, {'error' => 'expired_ticket'}.to_json]
67
+ else
68
+ [200, {'identifier' => ticket.identifier}.to_json]
69
+ end
70
+ rescue => e
71
+ [406, {'error' => 'bad_ticket'}.to_json]
72
+ end
73
+ end
74
+
75
+ # Takes incoming requests and generates an encrypted and Base64 encoded ticket in the 'ticket'
76
+ # property of the JSON response if the user has a valid session.
77
+ get '/session' do
78
+ content_type "application/json"
79
+ if session['identifier']
80
+ ticket = granter.generate_ticket(:identifier => session['identifier'])
81
+ [200, {'ticket' => Base64.encode64(ticket.to_encrypted)}.to_json]
82
+ else
83
+ [401, {'error' => 'authentication_required'}.to_json]
84
+ end
85
+ end
86
+
87
+ # Creates a session if authentication with the given credentials passed as 'identifier' and
88
+ # 'password' in the JSON body is successful.
89
+ post '/session' do
90
+ credentials = @json['credentials']
91
+ content_type "application/json"
92
+ if credentials.nil? || !granter.authenticate(:identifier => credentials['identifier'], :password => credentials['password'])
93
+ [406, {'error' => 'bad_credentials'}.to_json]
94
+ else
95
+ session['identifier'] = credentials['identifier']
96
+ [201, {'success' => 'session_created'}.to_json]
97
+ end
98
+ end
99
+
100
+ # Destroys an existing session.
101
+ delete '/session' do
102
+ request.session.clear
103
+ [200, {'success' => 'session_destroyed'}.to_json]
104
+ end
105
+
106
+ private
107
+
108
+ def auth
109
+ @auth ||= Rack::Auth::Basic::Request.new(request.env)
110
+ end
111
+
112
+ def authorized?
113
+ request.env['REMOTE_USER']
114
+ end
115
+
116
+ def unauthorized!(realm = "Kennedy")
117
+ headers 'WWW-Authenticate' => %(Basic realm="#{realm}")
118
+ throw :halt, [401, {'error' => 'authentication_required'}.to_json]
119
+ end
120
+
121
+ def bad_request!
122
+ throw :halt, [400, {'error' => 'bad_request'}.to_json]
123
+ end
124
+
125
+ def authorize(username, password)
126
+ api_keys.has_key?(username) && api_keys[username] == password
127
+ end
128
+
129
+ def require_api_authentication
130
+ return if authorized?
131
+ unauthorized! unless auth.provided?
132
+ bad_request! unless auth.basic?
133
+ unauthorized! unless authorize(*auth.credentials)
134
+ request.env['REMOTE_USER'] = auth.username
135
+ end
136
+
137
+ def self.add_cookie_middleware
138
+ use Rack::Session::Cookie, :secret => session_secret
139
+ end
140
+
141
+ def granter
142
+ @granter ||= Kennedy::Granter.new(:iv => encryption[:iv], :passphrase => encryption[:passphrase],
143
+ :backend => backend)
144
+ end
145
+
146
+ def encryption
147
+ self.class.encryption
148
+ end
149
+
150
+ def backend
151
+ self.class.backend
152
+ end
153
+
154
+ def api_keys
155
+ self.class.api_keys
156
+ end
157
+
158
+ def require_ssl?
159
+ !!options.require_ssl
160
+ end
161
+
162
+ end # Server
163
+ end # Kennedy
164
+