cloudkit-jruby 0.11.2

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 (64) hide show
  1. data/CHANGES +47 -0
  2. data/COPYING +20 -0
  3. data/README +84 -0
  4. data/Rakefile +42 -0
  5. data/TODO +21 -0
  6. data/cloudkit.gemspec +89 -0
  7. data/doc/curl.html +388 -0
  8. data/doc/images/example-code.gif +0 -0
  9. data/doc/images/json-title.gif +0 -0
  10. data/doc/images/oauth-discovery-logo.gif +0 -0
  11. data/doc/images/openid-logo.gif +0 -0
  12. data/doc/index.html +90 -0
  13. data/doc/main.css +151 -0
  14. data/doc/rest-api.html +467 -0
  15. data/examples/1.ru +3 -0
  16. data/examples/2.ru +3 -0
  17. data/examples/3.ru +6 -0
  18. data/examples/4.ru +5 -0
  19. data/examples/5.ru +9 -0
  20. data/examples/6.ru +11 -0
  21. data/examples/TOC +17 -0
  22. data/lib/cloudkit.rb +92 -0
  23. data/lib/cloudkit/constants.rb +34 -0
  24. data/lib/cloudkit/exceptions.rb +10 -0
  25. data/lib/cloudkit/flash_session.rb +20 -0
  26. data/lib/cloudkit/oauth_filter.rb +266 -0
  27. data/lib/cloudkit/oauth_store.rb +48 -0
  28. data/lib/cloudkit/openid_filter.rb +236 -0
  29. data/lib/cloudkit/openid_store.rb +100 -0
  30. data/lib/cloudkit/rack/builder.rb +120 -0
  31. data/lib/cloudkit/rack/router.rb +20 -0
  32. data/lib/cloudkit/request.rb +177 -0
  33. data/lib/cloudkit/service.rb +162 -0
  34. data/lib/cloudkit/store.rb +349 -0
  35. data/lib/cloudkit/store/memory_table.rb +99 -0
  36. data/lib/cloudkit/store/resource.rb +269 -0
  37. data/lib/cloudkit/store/response.rb +52 -0
  38. data/lib/cloudkit/store/response_helpers.rb +84 -0
  39. data/lib/cloudkit/templates/authorize_request_token.erb +19 -0
  40. data/lib/cloudkit/templates/oauth_descriptor.erb +43 -0
  41. data/lib/cloudkit/templates/oauth_meta.erb +8 -0
  42. data/lib/cloudkit/templates/openid_login.erb +31 -0
  43. data/lib/cloudkit/templates/request_authorization.erb +23 -0
  44. data/lib/cloudkit/templates/request_token_denied.erb +18 -0
  45. data/lib/cloudkit/uri.rb +88 -0
  46. data/lib/cloudkit/user_store.rb +37 -0
  47. data/lib/cloudkit/util.rb +25 -0
  48. data/spec/ext_spec.rb +76 -0
  49. data/spec/flash_session_spec.rb +20 -0
  50. data/spec/memory_table_spec.rb +86 -0
  51. data/spec/oauth_filter_spec.rb +326 -0
  52. data/spec/oauth_store_spec.rb +10 -0
  53. data/spec/openid_filter_spec.rb +81 -0
  54. data/spec/openid_store_spec.rb +101 -0
  55. data/spec/rack_builder_spec.rb +39 -0
  56. data/spec/request_spec.rb +191 -0
  57. data/spec/resource_spec.rb +310 -0
  58. data/spec/service_spec.rb +1039 -0
  59. data/spec/spec_helper.rb +32 -0
  60. data/spec/store_spec.rb +10 -0
  61. data/spec/uri_spec.rb +93 -0
  62. data/spec/user_store_spec.rb +10 -0
  63. data/spec/util_spec.rb +11 -0
  64. metadata +180 -0
@@ -0,0 +1,3 @@
1
+ $:.unshift File.expand_path(File.dirname(__FILE__)) + '/../lib'
2
+ require 'cloudkit'
3
+ expose :notes
@@ -0,0 +1,3 @@
1
+ $:.unshift File.expand_path(File.dirname(__FILE__)) + '/../lib'
2
+ require 'cloudkit'
3
+ contain :notes
@@ -0,0 +1,6 @@
1
+ $:.unshift File.expand_path(File.dirname(__FILE__)) + '/../lib'
2
+ require 'cloudkit'
3
+ use Rack::Session::Pool
4
+ use CloudKit::OpenIDFilter
5
+ use CloudKit::Service, :collections => [:notes]
6
+ run lambda{|env| [200, {'Content-Type' => 'text/html', 'Content-Length' => '5'}, ['HELLO']]}
@@ -0,0 +1,5 @@
1
+ $:.unshift File.expand_path(File.dirname(__FILE__)) + '/../lib'
2
+ require 'cloudkit'
3
+ use CloudKit::OAuthFilter
4
+ use CloudKit::Service, :collections => [:notes]
5
+ run lambda{|env| [200, {'Content-Type' => 'text/html', 'Content-Length' => '5'}, ['HELLO']]}
@@ -0,0 +1,9 @@
1
+ $:.unshift File.expand_path(File.dirname(__FILE__)) + '/../lib'
2
+ require 'cloudkit'
3
+ require 'rufus/tokyo' # gem install rufus-tokyo
4
+ CloudKit.setup_storage_adapter(Rufus::Tokyo::Table.new('cloudkit.tdb'))
5
+ use Rack::Session::Pool
6
+ use CloudKit::OAuthFilter
7
+ use CloudKit::OpenIDFilter
8
+ use CloudKit::Service, :collections => [:notes]
9
+ run lambda{|env| [200, {'Content-Type' => 'text/html', 'Content-Length' => '5'}, ['HELLO']]}
@@ -0,0 +1,11 @@
1
+ $:.unshift File.expand_path(File.dirname(__FILE__)) + '/../lib'
2
+ require 'cloudkit'
3
+ require 'rufus/tokyo/tyrant' # gem install rufus-tokyo
4
+ # start Tokyo Tyrant with a table store...
5
+ # ttserver data.tct
6
+ CloudKit.setup_storage_adapter(Rufus::Tokyo::TyrantTable.new('127.0.0.1', 1978))
7
+ use Rack::Session::Pool
8
+ use CloudKit::OAuthFilter
9
+ use CloudKit::OpenIDFilter
10
+ use CloudKit::Service, :collections => [:notes]
11
+ run lambda{|env| [200, {'Content-Type' => 'text/html', 'Content-Length' => '5'}, ['HELLO']]}
@@ -0,0 +1,17 @@
1
+ Index of Examples
2
+ -----------------
3
+
4
+ When using the gem version of CloudKit, the first line of each example can be
5
+ removed.
6
+
7
+ 1. Expose Notes - Mount a JSON "notes" API. Uses in-memory store.
8
+
9
+ 2. Contain Notes - Same as #1, adding OpenID and OAuth.
10
+
11
+ 3. Notes with OpenID - Same as #1 using only OpenID.
12
+
13
+ 4. Notes with OAuth - Same as #1 using only OAuth.
14
+
15
+ 5. Tokyo Notes - Same as #2 with a Tokyo Cabinet Table store.
16
+
17
+ 6. Tyrant Notes - Same as #2 with a Tokyo Tyrant Table store.
@@ -0,0 +1,92 @@
1
+ require 'rubygems'
2
+ require 'erb'
3
+ require 'json'
4
+ require 'digest/md5'
5
+ require 'openid'
6
+ require 'time'
7
+ require 'uuid'
8
+ require 'rack'
9
+ require 'oauth'
10
+ require 'oauth/consumer'
11
+ require 'oauth/request_proxy/rack_request'
12
+ require 'oauth/server'
13
+ require 'oauth/signature'
14
+ require 'cloudkit/constants'
15
+ require 'cloudkit/exceptions'
16
+ require 'cloudkit/util'
17
+ require 'cloudkit/uri'
18
+ require 'cloudkit/store/memory_table'
19
+ require 'cloudkit/store/resource'
20
+ require 'cloudkit/store/response'
21
+ require 'cloudkit/store/response_helpers'
22
+ require 'cloudkit/store'
23
+ require 'cloudkit/flash_session'
24
+ require 'cloudkit/oauth_filter'
25
+ require 'cloudkit/oauth_store'
26
+ require 'cloudkit/openid_filter'
27
+ require 'cloudkit/openid_store'
28
+ require 'cloudkit/rack/builder'
29
+ require 'cloudkit/rack/router'
30
+ require 'cloudkit/request'
31
+ require 'cloudkit/service'
32
+ require 'cloudkit/user_store'
33
+
34
+ include CloudKit::Constants
35
+
36
+ module CloudKit
37
+ VERSION = '0.11.2'
38
+
39
+ # Sets up the storage adapter. Defaults to development-time
40
+ # CloudKit::MemoryTable. Also supports Rufus Tokyo Table instances. See the
41
+ # examples directory for Cabinet and Tyrant Table examples.
42
+ def self.setup_storage_adapter(adapter_instance=nil)
43
+ @storage_adapter = adapter_instance || CloudKit::MemoryTable.new
44
+ end
45
+
46
+ # Return the shared storage adapter.
47
+ def self.storage_adapter
48
+ @storage_adapter
49
+ end
50
+ end
51
+
52
+ class Object
53
+
54
+ # Execute a method if it exists.
55
+ def try(method) # via defunkt
56
+ send method if respond_to? method
57
+ end
58
+ end
59
+
60
+ class Hash
61
+
62
+ # For each key in 'other' that has a non-nil value, merge it into the current
63
+ # Hash.
64
+ def filter_merge!(other={})
65
+ other.each_pair{|k,v| self.merge!(k => v) unless v.nil?}
66
+ self
67
+ end
68
+
69
+ # Change the key 'oldkey' to 'newkey'
70
+ def rekey!(oldkey, newkey)
71
+ if self.has_key? oldkey
72
+ self[newkey] = self.delete(oldkey)
73
+ end
74
+ nil
75
+ end
76
+
77
+ # Return a new Hash, excluding the specified list of keys.
78
+ def excluding(*keys)
79
+ trimmed = self.dup
80
+ keys.each{|k| trimmed.delete(k)}
81
+ trimmed
82
+ end
83
+ end
84
+
85
+ class Array
86
+
87
+ # Return a new Array, excluding the specified list of values.
88
+ def excluding(*keys)
89
+ trimmed = self.dup
90
+ trimmed - keys
91
+ end
92
+ end
@@ -0,0 +1,34 @@
1
+ module CloudKit
2
+ module Constants
3
+
4
+ # The key used to store the authenticated user.
5
+ CLOUDKIT_AUTH_KEY = 'cloudkit.user'.freeze
6
+
7
+ # The key used to indicate the presence of auth in a stack.
8
+ CLOUDKIT_AUTH_PRESENCE = 'cloudkit.auth'.freeze
9
+
10
+ # The key used to store auth challenge headers shared between
11
+ # OpenID and OAuth middleware.
12
+ CLOUDKIT_AUTH_CHALLENGE = 'cloudkit.challenge'.freeze
13
+
14
+ # The 'via' key used to announce and track upstream middleware.
15
+ CLOUDKIT_VIA = 'cloudkit.via'.freeze
16
+
17
+ # The key used to store the 'flash' in the session.
18
+ CLOUDKIT_FLASH = 'cloudkit.flash'.freeze
19
+
20
+ # The 'via' key for the OAuth filter.
21
+ CLOUDKIT_OAUTH_FILTER_KEY = 'cloudkit.filter.oauth'.freeze
22
+
23
+ # The 'via' key for the OpenID filter.
24
+ CLOUDKIT_OPENID_FILTER_KEY = 'cloudkit.filter.openid'.freeze
25
+
26
+ # The key for the login URL used in OpenID and OAuth middleware
27
+ # components.
28
+ CLOUDKIT_LOGIN_URL = 'cloudkit.filter.openid.url.login'.freeze
29
+
30
+ # The key for the logout URL used in OpenID and OAuth middleware
31
+ # components.
32
+ CLOUDKIT_LOGOUT_URL = 'cloudkit.filter.openid.url.logout'.freeze
33
+ end
34
+ end
@@ -0,0 +1,10 @@
1
+ module CloudKit
2
+
3
+ # HistoricalIntegrityViolation exceptions are raised when an attempt is made
4
+ # to modify an archived or deleted version of a resource.
5
+ class HistoricalIntegrityViolation < Exception; end
6
+
7
+ # InvalidURIFormat exceptions are raised during attempts to get or generate
8
+ # cannonical URIs from non-collection or non-resource URIs.
9
+ class InvalidURIFormat < Exception; end
10
+ end
@@ -0,0 +1,20 @@
1
+ module CloudKit
2
+
3
+ # FlashSessions are hashes that forget their contents after the first access.
4
+ # Useful for session-based messaging.
5
+ class FlashSession
6
+ def initialize
7
+ @values = {}
8
+ end
9
+
10
+ # Set the value for a key.
11
+ def []=(k, v)
12
+ @values[k] = v
13
+ end
14
+
15
+ # Access a value, then forget it.
16
+ def [](k)
17
+ @values.delete(k)
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,266 @@
1
+ module CloudKit
2
+
3
+ # An OAuthFilter provides both OAuth 1.0 support, plus OAuth Discovery.
4
+ #
5
+ # Responds to the following URIs as part of the OAuth 1.0 "dance":
6
+ #
7
+ # /oauth/request_tokens
8
+ # /oauth/authorization
9
+ # /oauth/authorized_request_tokens/{id}
10
+ # /oauth/access_tokens
11
+ #
12
+ # Responds to the following URIs as part of OAuth Discovery:
13
+ # /oauth
14
+ # /oauth/meta
15
+ #
16
+ # See also:
17
+ # - {OAuth Core 1.0}[http://oauth.net/core/1.0]
18
+ # - {OAuth Discovery}[http://oauth.net/discovery]
19
+ # - Thread[http://groups.google.com/group/oauth/browse_thread/thread/29a1b550396f63cf] covering /oauth and /oauth/meta URIs.
20
+ #
21
+ class OAuthFilter
22
+ include Util
23
+
24
+ @@lock = Mutex.new
25
+ @@store = nil
26
+
27
+ def initialize(app, options={})
28
+ @app = app
29
+ @options = options
30
+ end
31
+
32
+ def call(env)
33
+ @@lock.synchronize do
34
+ @@store = OAuthStore.new
35
+ end unless @@store
36
+
37
+ request = Request.new(env)
38
+ request.announce_auth(CLOUDKIT_OAUTH_FILTER_KEY)
39
+ return xrds_location(request) if oauth_disco_draft2_xrds?(request)
40
+ return @app.call(env) if request.path_info == '/'
41
+
42
+ load_user_from_session(request)
43
+
44
+ begin
45
+ case request
46
+ when r(:get, '/oauth/meta')
47
+ get_meta(request)
48
+ when r(:post, '/oauth/request_tokens', ['oauth_consumer_key'])
49
+ create_request_token(request)
50
+ when r(:get, '/oauth/authorization', ['oauth_token'])
51
+ request_authorization(request)
52
+ when r(:put, '/oauth/authorized_request_tokens/:id', ['submit' => 'Approve'])
53
+ # Temporarily relying on a button value until pluggable templates are
54
+ # introduced in 1.0.
55
+ authorize_request_token(request)
56
+ when r(:put, '/oauth/authorized_request_tokens/:id', ['submit' => 'Deny'])
57
+ # See previous comment.
58
+ deny_request_token(request)
59
+ when r(:post, '/oauth/authorized_request_tokens/:id', [{'_method' => 'PUT'}])
60
+ authorize_request_token(request)
61
+ when r(:post, '/oauth/access_tokens')
62
+ create_access_token(request)
63
+ when r(:get, '/oauth')
64
+ get_descriptor(request)
65
+ else
66
+ inject_user_or_challenge(request)
67
+ @app.call(env)
68
+ end
69
+ rescue OAuth::Signature::UnknownSignatureMethod
70
+ # The OAuth spec suggests a 400 status, but serving a 401 with the
71
+ # meta/challenge info seems more appropriate as the OAuth metadata
72
+ # specifies the supported signature methods, giving the user agent an
73
+ # opportunity to fix the error.
74
+ return challenge(request, 'unknown signature method')
75
+ end
76
+ end
77
+
78
+ def store; @@store; end
79
+
80
+ protected
81
+
82
+ def create_request_token(request)
83
+ return challenge(request, 'invalid nonce') unless valid_nonce?(request)
84
+
85
+ consumer_result = @@store.get("/cloudkit_oauth_consumers/#{request[:oauth_consumer_key]}")
86
+ unless consumer_result.status == 200
87
+ return challenge(request, 'invalid consumer')
88
+ end
89
+
90
+ consumer = consumer_result.parsed_content
91
+ signature = OAuth::Signature.build(request) { [nil, consumer['secret']] }
92
+ return challenge(request, 'invalid signature') unless signature.verify
93
+
94
+ token_id, secret = OAuth::Server.new(request.host).generate_credentials
95
+ request_token = JSON.generate(
96
+ :secret => secret,
97
+ :consumer_key => request[:oauth_consumer_key])
98
+ @@store.put(
99
+ "/cloudkit_oauth_request_tokens/#{token_id}",
100
+ :json => request_token)
101
+ Rack::Response.new("oauth_token=#{token_id}&oauth_token_secret=#{secret}", 201).finish
102
+ end
103
+
104
+ def request_authorization(request)
105
+ return login_redirect(request) unless request.current_user
106
+
107
+ request_token_result = @@store.get("/cloudkit_oauth_request_tokens/#{request[:oauth_token]}")
108
+ unless request_token_result.status == 200
109
+ return challenge(request, 'invalid request token')
110
+ end
111
+
112
+ request_token = request_token_result.parsed_content
113
+ erb(request, :request_authorization)
114
+ end
115
+
116
+ def authorize_request_token(request)
117
+ return login_redirect(request) unless request.current_user
118
+
119
+ request_token_response = @@store.get("/cloudkit_oauth_request_tokens/#{request.last_path_element}")
120
+ request_token = request_token_response.parsed_content
121
+ if request_token['authorized_at']
122
+ return challenge(request, 'invalid request token')
123
+ end
124
+
125
+ request_token['user_id'] = request.current_user
126
+ request_token['authorized_at'] = Time.now.httpdate
127
+ json = JSON.generate(request_token)
128
+ @@store.put(
129
+ "/cloudkit_oauth_request_tokens/#{request.last_path_element}",
130
+ :etag => request_token_response.etag,
131
+ :json => json)
132
+ erb(request, :authorize_request_token)
133
+ end
134
+
135
+ def deny_request_token(request)
136
+ return login_redirect(request) unless request.current_user
137
+
138
+ request_token_response = @@store.get("/cloudkit_oauth_request_tokens/#{request.last_path_element}")
139
+ @@store.delete(
140
+ "/cloudkit_oauth_request_tokens/#{request.last_path_element}",
141
+ :etag => request_token_response.etag)
142
+ erb(request, :request_token_denied)
143
+ end
144
+
145
+ def create_access_token(request)
146
+ return challenge(request, 'invalid nonce') unless valid_nonce?(request)
147
+
148
+ consumer_response = @@store.get("/cloudkit_oauth_consumers/#{request[:oauth_consumer_key]}")
149
+ unless consumer_response.status == 200
150
+ return challenge(request, 'invalid consumer')
151
+ end
152
+
153
+ consumer = consumer_response.parsed_content
154
+ request_token_response = @@store.get("/cloudkit_oauth_request_tokens/#{request[:oauth_token]}")
155
+ unless request_token_response.status == 200
156
+ return challenge(request, 'invalid request token')
157
+ end
158
+
159
+ request_token = request_token_response.parsed_content
160
+ unless request_token['consumer_key'] == request[:oauth_consumer_key]
161
+ return challenge(request, 'invalid consumer')
162
+ end
163
+
164
+ signature = OAuth::Signature.build(request) do
165
+ [request_token['secret'], consumer['secret']]
166
+ end
167
+ unless signature.verify
168
+ return challenge(request, 'invalid signature')
169
+ end
170
+
171
+ token_id, secret = OAuth::Server.new(request.host).generate_credentials
172
+ token_data = JSON.generate(
173
+ :secret => secret,
174
+ :consumer_key => request[:oauth_consumer_key],
175
+ :consumer_secret => consumer['secret'],
176
+ :user_id => request_token['user_id'])
177
+ @@store.put("/cloudkit_oauth_tokens/#{token_id}", :json => token_data)
178
+ @@store.delete(
179
+ "/cloudkit_oauth_request_tokens/#{request[:oauth_token]}",
180
+ :etag => request_token_response.etag)
181
+ Rack::Response.new("oauth_token=#{token_id}&oauth_token_secret=#{secret}", 201).finish
182
+ end
183
+
184
+ def inject_user_or_challenge(request)
185
+ unless valid_nonce?(request)
186
+ request.current_user = ''
187
+ inject_challenge(request)
188
+ return
189
+ end
190
+
191
+ result = @@store.get("/cloudkit_oauth_tokens/#{request[:oauth_token]}")
192
+ access_token = result.parsed_content
193
+ signature = OAuth::Signature.build(request) do
194
+ [access_token['secret'], access_token['consumer_secret']]
195
+ end
196
+ if signature.verify
197
+ request.current_user = access_token['user_id']
198
+ else
199
+ request.current_user = ''
200
+ inject_challenge(request)
201
+ end
202
+ end
203
+
204
+ def valid_nonce?(request)
205
+ timestamp = request[:oauth_timestamp]
206
+ nonce = request[:oauth_nonce]
207
+ return false unless (timestamp && nonce)
208
+
209
+ uri = "/cloudkit_oauth_nonces/#{timestamp},#{nonce}"
210
+ result = @@store.put(uri, :json => '{}')
211
+ return false unless result.status == 201
212
+
213
+ true
214
+ end
215
+
216
+ def inject_challenge(request)
217
+ request.env[CLOUDKIT_AUTH_CHALLENGE] = challenge_headers(request)
218
+ end
219
+
220
+ def challenge(request, message='')
221
+ Rack::Response.new(message, 401, challenge_headers(request)).finish
222
+ end
223
+
224
+ def challenge_headers(request)
225
+ {
226
+ 'WWW-Authenticate' => "OAuth realm=\"http://#{request.env['HTTP_HOST']}\"",
227
+ 'Link' => discovery_link(request),
228
+ 'Content-Type' => 'application/json'
229
+ }
230
+ end
231
+
232
+ def discovery_link(request)
233
+ "<#{request.scheme}://#{request.env['HTTP_HOST']}/oauth/meta>; rel=\"http://oauth.net/discovery/1.0/rel/provider\""
234
+ end
235
+
236
+ def login_redirect(request)
237
+ request.session['return_to'] = request.url if request.session
238
+ Rack::Response.new([], 302, {'Location' => request.login_url}).finish
239
+ end
240
+
241
+ def load_user_from_session(request)
242
+ request.current_user = request.session['user_uri'] if request.session
243
+ end
244
+
245
+ def get_meta(request)
246
+ # Expected in next OAuth Discovery Draft
247
+ erb(request, :oauth_meta)
248
+ end
249
+
250
+ def oauth_disco_draft2_xrds?(request)
251
+ # Current OAuth Discovery Draft 2 / XRDS-Simple 1.0, Section 5.1.2
252
+ request.get? &&
253
+ request.env['HTTP_ACCEPT'] &&
254
+ request.env['HTTP_ACCEPT'].match(/application\/xrds\+xml/)
255
+ end
256
+
257
+ def xrds_location(request)
258
+ # Current OAuth Discovery Draft 2 / XRDS-Simple 1.0, Section 5.1.2
259
+ Rack::Response.new([], 200, {'X-XRDS-Location' => "#{request.scheme}://#{request.env['HTTP_HOST']}/oauth"}).finish
260
+ end
261
+
262
+ def get_descriptor(request)
263
+ erb(request, :oauth_descriptor, {'Content-Type' => 'application/xrds+xml'})
264
+ end
265
+ end
266
+ end