cloudkit 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGES +2 -0
- data/COPYING +20 -0
- data/README +55 -0
- data/Rakefile +35 -0
- data/TODO +22 -0
- data/cloudkit.gemspec +82 -0
- data/doc/curl.html +329 -0
- data/doc/images/example-code.gif +0 -0
- data/doc/images/json-title.gif +0 -0
- data/doc/images/oauth-discovery-logo.gif +0 -0
- data/doc/images/openid-logo.gif +0 -0
- data/doc/index.html +87 -0
- data/doc/main.css +151 -0
- data/doc/rest-api.html +358 -0
- data/examples/1.ru +3 -0
- data/examples/2.ru +3 -0
- data/examples/3.ru +6 -0
- data/examples/4.ru +5 -0
- data/examples/5.ru +10 -0
- data/examples/6.ru +10 -0
- data/examples/TOC +17 -0
- data/lib/cloudkit.rb +74 -0
- data/lib/cloudkit/flash_session.rb +22 -0
- data/lib/cloudkit/oauth_filter.rb +273 -0
- data/lib/cloudkit/oauth_store.rb +56 -0
- data/lib/cloudkit/openid_filter.rb +198 -0
- data/lib/cloudkit/openid_store.rb +101 -0
- data/lib/cloudkit/rack/builder.rb +120 -0
- data/lib/cloudkit/rack/router.rb +20 -0
- data/lib/cloudkit/request.rb +159 -0
- data/lib/cloudkit/service.rb +135 -0
- data/lib/cloudkit/store.rb +459 -0
- data/lib/cloudkit/store/adapter.rb +9 -0
- data/lib/cloudkit/store/extraction_view.rb +57 -0
- data/lib/cloudkit/store/response.rb +51 -0
- data/lib/cloudkit/store/response_helpers.rb +72 -0
- data/lib/cloudkit/store/sql_adapter.rb +36 -0
- data/lib/cloudkit/templates/authorize_request_token.erb +19 -0
- data/lib/cloudkit/templates/oauth_descriptor.erb +43 -0
- data/lib/cloudkit/templates/oauth_meta.erb +8 -0
- data/lib/cloudkit/templates/openid_login.erb +31 -0
- data/lib/cloudkit/templates/request_authorization.erb +23 -0
- data/lib/cloudkit/templates/request_token_denied.erb +18 -0
- data/lib/cloudkit/user_store.rb +44 -0
- data/lib/cloudkit/util.rb +60 -0
- data/test/ext_test.rb +57 -0
- data/test/flash_session_test.rb +22 -0
- data/test/helper.rb +50 -0
- data/test/oauth_filter_test.rb +331 -0
- data/test/oauth_store_test.rb +12 -0
- data/test/openid_filter_test.rb +54 -0
- data/test/openid_store_test.rb +12 -0
- data/test/rack_builder_test.rb +41 -0
- data/test/request_test.rb +197 -0
- data/test/service_test.rb +718 -0
- data/test/store_test.rb +99 -0
- data/test/user_store_test.rb +12 -0
- data/test/util_test.rb +13 -0
- metadata +190 -0
@@ -0,0 +1,198 @@
|
|
1
|
+
module CloudKit
|
2
|
+
|
3
|
+
# An OpenIDFilter provides OpenID authentication, listening for upstream
|
4
|
+
# OAuth authentication and bypassing if already authorized.
|
5
|
+
#
|
6
|
+
# Responds to the following URIs:
|
7
|
+
# /login
|
8
|
+
# /logout
|
9
|
+
# /openid_complete
|
10
|
+
#
|
11
|
+
class OpenIDFilter
|
12
|
+
include Util
|
13
|
+
|
14
|
+
@@lock = Mutex.new
|
15
|
+
@@store = nil
|
16
|
+
|
17
|
+
def initialize(app, options={})
|
18
|
+
@app = app; @options = options
|
19
|
+
end
|
20
|
+
|
21
|
+
def call(env)
|
22
|
+
@@lock.synchronize do
|
23
|
+
@@store = OpenIDStore.new(env[storage_uri_key])
|
24
|
+
@users = UserStore.new(env[storage_uri_key])
|
25
|
+
@@store.get_association('x') rescue nil # refresh sqlite3
|
26
|
+
end unless @@store
|
27
|
+
|
28
|
+
request = Request.new(env)
|
29
|
+
request.announce_auth(openid_filter_key)
|
30
|
+
|
31
|
+
case request
|
32
|
+
when r(:get, request.login_url); request_login(request)
|
33
|
+
when r(:post, request.login_url); begin_openid_login(request)
|
34
|
+
when r(:get, '/openid_complete'); complete_openid_login(request)
|
35
|
+
when r(:post, request.logout_url); logout(request)
|
36
|
+
else
|
37
|
+
if (root_request?(request) || valid_auth_key?(request) || logged_in?(request))
|
38
|
+
@app.call(env)
|
39
|
+
else
|
40
|
+
if request.env[challenge_key]
|
41
|
+
store_location(request)
|
42
|
+
erb(request, :openid_login, request.env[challenge_key], 401)
|
43
|
+
elsif !request.via.include?(oauth_filter_key)
|
44
|
+
store_location(request)
|
45
|
+
login_redirect(request)
|
46
|
+
else
|
47
|
+
[500, {}, ['server misconfigured']]
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def logout(request)
|
54
|
+
user_uri = request.session.delete('user_uri')
|
55
|
+
result = @users.get(user_uri)
|
56
|
+
user = result.parsed_content
|
57
|
+
user.delete('remember_me_token')
|
58
|
+
user.delete('remember_me_expiration')
|
59
|
+
json = JSON.generate(user)
|
60
|
+
@users.put(user_uri, :etag => result.etag, :json => json)
|
61
|
+
|
62
|
+
request.env[auth_key] = nil
|
63
|
+
request.flash['info'] = 'You have been logged out.'
|
64
|
+
response = Rack::Response.new([], 302, {'Location' => request.login_url})
|
65
|
+
response.delete_cookie('remember_me')
|
66
|
+
response.finish
|
67
|
+
end
|
68
|
+
|
69
|
+
def request_login(request)
|
70
|
+
erb(request, :openid_login)
|
71
|
+
end
|
72
|
+
|
73
|
+
def begin_openid_login(request)
|
74
|
+
begin
|
75
|
+
response = openid_consumer(request).begin request[:openid_url]
|
76
|
+
rescue => e
|
77
|
+
request.flash[:error] = e
|
78
|
+
return login_redirect(request)
|
79
|
+
end
|
80
|
+
|
81
|
+
redirect_url = response.redirect_url(base_url(request), full_url(request))
|
82
|
+
[302, {'Location' => redirect_url}, []]
|
83
|
+
end
|
84
|
+
|
85
|
+
def complete_openid_login(request)
|
86
|
+
begin
|
87
|
+
idp_response = openid_consumer(request).complete(request.params, full_url(request))
|
88
|
+
rescue => e
|
89
|
+
request.flash[:error] = e
|
90
|
+
return login_redirect(request)
|
91
|
+
end
|
92
|
+
|
93
|
+
if idp_response.is_a?(OpenID::Consumer::FailureResponse)
|
94
|
+
request.flash[:error] = idp_response.message
|
95
|
+
return login_redirect(request)
|
96
|
+
end
|
97
|
+
|
98
|
+
result = @users.get(
|
99
|
+
'/cloudkit_login_view',
|
100
|
+
:identity_url => idp_response.endpoint.claimed_id)
|
101
|
+
user_uris = result.parsed_content['uris']
|
102
|
+
|
103
|
+
if user_uris.empty?
|
104
|
+
json = JSON.generate(:identity_url => idp_response.endpoint.claimed_id)
|
105
|
+
result = @users.post('/cloudkit_users', :json => json)
|
106
|
+
user_uri = result.parsed_content['uri']
|
107
|
+
else
|
108
|
+
user_uri = user_uris.first
|
109
|
+
end
|
110
|
+
user_result = @users.resolve_uris([user_uri]).first
|
111
|
+
user = user_result.parsed_content
|
112
|
+
|
113
|
+
if request.session['user_uri'] = user_uri
|
114
|
+
request.current_user = user_uri
|
115
|
+
user['remember_me_expiration'] = two_weeks_from_now
|
116
|
+
user['remember_me_token'] = Base64.encode64(
|
117
|
+
OpenSSL::Random.random_bytes(32)).gsub(/\W/,'')
|
118
|
+
url = request.session.delete('return_to')
|
119
|
+
response = Rack::Response.new([], 302, {'Location' => (url || '/')})
|
120
|
+
response.set_cookie(
|
121
|
+
'remember_me', {
|
122
|
+
:value => user['remember_me_token'],
|
123
|
+
:expires => Time.at(user['remember_me_expiration']).utc})
|
124
|
+
json = JSON.generate(user)
|
125
|
+
@users.put(user_uri, :etag => user_result.etag, :json => json)
|
126
|
+
request.flash[:notice] = 'You have been logged in.'
|
127
|
+
response.finish
|
128
|
+
else
|
129
|
+
request.flash[:error] = 'Could not log on with your OpenID.'
|
130
|
+
login_redirect(request)
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
def login_redirect(request)
|
135
|
+
[302, {'Location' => request.login_url}, []]
|
136
|
+
end
|
137
|
+
|
138
|
+
def base_url(request)
|
139
|
+
"#{request.scheme}://#{request.env['HTTP_HOST']}/"
|
140
|
+
end
|
141
|
+
|
142
|
+
def full_url(request)
|
143
|
+
base_url(request) + 'openid_complete'
|
144
|
+
end
|
145
|
+
|
146
|
+
def logged_in?(request)
|
147
|
+
logged_in = user_in_session?(request) || valid_remember_me_token?(request)
|
148
|
+
request.current_user = request.session['user_uri'] if logged_in
|
149
|
+
logged_in
|
150
|
+
end
|
151
|
+
|
152
|
+
def user_in_session?(request)
|
153
|
+
request.session['user_uri'] != nil
|
154
|
+
end
|
155
|
+
|
156
|
+
def store_location(request)
|
157
|
+
request.session['return_to'] = request.url
|
158
|
+
end
|
159
|
+
|
160
|
+
def root_request?(request)
|
161
|
+
request.path_info == '/' || request.path_info == '/favicon.ico'
|
162
|
+
end
|
163
|
+
|
164
|
+
def valid_auth_key?(request)
|
165
|
+
request.env[auth_key] && request.env[auth_key] != ''
|
166
|
+
end
|
167
|
+
|
168
|
+
def openid_consumer(request)
|
169
|
+
@openid_consumer ||= OpenID::Consumer.new(
|
170
|
+
request.session, OpenIDStore.new)
|
171
|
+
end
|
172
|
+
|
173
|
+
def valid_remember_me_token?(request)
|
174
|
+
return false unless token = request.cookies['remember_me']
|
175
|
+
|
176
|
+
result = @users.get('/cloudkit_login_view', :remember_me_token => token)
|
177
|
+
return false unless result.status == 200
|
178
|
+
|
179
|
+
user_uris = result.parsed_content['uris']
|
180
|
+
return false unless user_uris.try(:size) == 1
|
181
|
+
|
182
|
+
user_uri = user_uris.first
|
183
|
+
user_result = @users.resolve_uris([user_uri]).first
|
184
|
+
user = user_result.parsed_content
|
185
|
+
return false unless Time.now.to_i < user['remember_me_expiration']
|
186
|
+
|
187
|
+
user['remember_me_expiration'] = two_weeks_from_now
|
188
|
+
json = JSON.generate(user)
|
189
|
+
@users.put(user_uri, :etag => user_result.etag, :json => json)
|
190
|
+
request.session['user_uri'] = user_uri
|
191
|
+
true
|
192
|
+
end
|
193
|
+
|
194
|
+
def two_weeks_from_now
|
195
|
+
Time.now.to_i+1209600
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
require 'openid/store/interface'
|
2
|
+
module CloudKit
|
3
|
+
|
4
|
+
# An OpenIDStore provides the interface expected by the ruby-openid gem,
|
5
|
+
# mapping it to a CloudKit::Store instance.
|
6
|
+
class OpenIDStore < OpenID::Store::Interface
|
7
|
+
@@store = nil
|
8
|
+
|
9
|
+
# Initialize an OpenIDStore and its required views.
|
10
|
+
def initialize(uri=nil)
|
11
|
+
unless @@store
|
12
|
+
association_view = ExtractionView.new(
|
13
|
+
:cloudkit_openid_server_handles,
|
14
|
+
:observe => :cloudkit_openid_associations,
|
15
|
+
:extract => [:server_url, :handle])
|
16
|
+
@@store = Store.new(
|
17
|
+
:collections => [:cloudkit_openid_associations, :cloudkit_openid_nonces],
|
18
|
+
:views => [association_view],
|
19
|
+
:adapter => SQLAdapter.new(uri))
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def get_association(server_url, handle=nil) #:nodoc:
|
24
|
+
options = {:server_url => server_url}
|
25
|
+
options.merge!(:handle => Base64.encode64(handle)) if (handle && handle != '')
|
26
|
+
result = @@store.get('/cloudkit_openid_server_handles', options)
|
27
|
+
return nil unless result.status == 200
|
28
|
+
return nil if result.parsed_content['total'] == 0
|
29
|
+
|
30
|
+
ignore, associations = resolve_associations(result.parsed_content)
|
31
|
+
return nil if associations.empty?
|
32
|
+
|
33
|
+
associations.sort_by{|a| a['issued']}
|
34
|
+
a = associations[-1]
|
35
|
+
OpenID::Association.new(
|
36
|
+
Base64.decode64(a['handle']),
|
37
|
+
Base64.decode64(a['secret']),
|
38
|
+
Time.at(a['issued']),
|
39
|
+
a['lifetime'],
|
40
|
+
a['assoc_type'])
|
41
|
+
end
|
42
|
+
|
43
|
+
def remove_association(server_url, handle) #:nodoc:
|
44
|
+
result = @@store.get(
|
45
|
+
'/cloudkit_openid_server_handles',
|
46
|
+
:server_url => server_url,
|
47
|
+
:handle => Base64.encode64(handle))
|
48
|
+
return nil unless result.status == 200
|
49
|
+
|
50
|
+
responses, associations = resolve_associations(result.parsed_content)
|
51
|
+
return nil if associations.empty?
|
52
|
+
|
53
|
+
uris = result.parsed_content['uris']
|
54
|
+
responses.each_with_index do |r, index|
|
55
|
+
@@store.delete(uris[index], :etag => r.etag)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def store_association(server_url, association) #:nodoc:
|
60
|
+
remove_association(server_url, association.handle)
|
61
|
+
json = JSON.generate(
|
62
|
+
:server_url => server_url,
|
63
|
+
:handle => Base64.encode64(association.handle),
|
64
|
+
:secret => Base64.encode64(association.secret),
|
65
|
+
:issued => association.issued.to_i,
|
66
|
+
:lifetime => association.lifetime,
|
67
|
+
:assoc_type => association.assoc_type)
|
68
|
+
result = @@store.post('/cloudkit_openid_associations', :json => json)
|
69
|
+
return (result.status == 201)
|
70
|
+
end
|
71
|
+
|
72
|
+
def use_nonce(server_url, timestamp, salt) #:nodoc:
|
73
|
+
return false if (timestamp - Time.now.to_i).abs > OpenID::Nonce.skew
|
74
|
+
|
75
|
+
fragment = URI.escape([server_url, timestamp, salt].join('-'))
|
76
|
+
uri = "/cloudkit_openid_nonces/#{fragment}"
|
77
|
+
result = @@store.put(uri, :json => '{}')
|
78
|
+
return (result.status == 201)
|
79
|
+
end
|
80
|
+
|
81
|
+
def self.cleanup #:nodoc:
|
82
|
+
# TODO
|
83
|
+
end
|
84
|
+
|
85
|
+
def self.cleanup_associations #:nodoc:
|
86
|
+
# TODO
|
87
|
+
end
|
88
|
+
|
89
|
+
def self.cleanup_nonces #:nodoc:
|
90
|
+
# TODO
|
91
|
+
end
|
92
|
+
|
93
|
+
protected
|
94
|
+
|
95
|
+
def resolve_associations(parsed_content) #:nodoc:
|
96
|
+
uri_list = parsed_content['uris']
|
97
|
+
association_responses = @@store.resolve_uris(uri_list)
|
98
|
+
return association_responses, association_responses.map{|a| a.parsed_content}
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,120 @@
|
|
1
|
+
module Rack #:nodoc:
|
2
|
+
class Builder
|
3
|
+
alias_method :cloudkit_to_app, :to_app
|
4
|
+
|
5
|
+
# Extends Rack::Builder's to_app method to detect if the last piece of
|
6
|
+
# middleware in the stack is a CloudKit shortcut (contain or expose), adding
|
7
|
+
# adding a default developer page at the root and a 404 everywhere else.
|
8
|
+
def to_app
|
9
|
+
default_app = lambda do |env|
|
10
|
+
if (env['PATH_INFO'] == '/')
|
11
|
+
[200, {'Content-Type' => 'text/html'}, [welcome]]
|
12
|
+
else
|
13
|
+
[404, {}, []]
|
14
|
+
end
|
15
|
+
end
|
16
|
+
@ins << default_app if @last_cloudkit_id == @ins.last.object_id
|
17
|
+
cloudkit_to_app
|
18
|
+
end
|
19
|
+
|
20
|
+
# Setup resource collections hosted behind OAuth and OpenID auth filters.
|
21
|
+
#
|
22
|
+
# ===Example
|
23
|
+
# contain :notes, :projects
|
24
|
+
#
|
25
|
+
def contain(*args)
|
26
|
+
@ins << lambda do |app|
|
27
|
+
Rack::Session::Pool.new(
|
28
|
+
CloudKit::OAuthFilter.new(
|
29
|
+
CloudKit::OpenIDFilter.new(
|
30
|
+
CloudKit::Service.new(app, :collections => args.to_a))))
|
31
|
+
end
|
32
|
+
@last_cloudkit_id = @ins.last.object_id
|
33
|
+
end
|
34
|
+
|
35
|
+
# Setup resource collections without authentication.
|
36
|
+
#
|
37
|
+
# ===Example
|
38
|
+
# expose :notes, :projects
|
39
|
+
#
|
40
|
+
def expose(*args)
|
41
|
+
@ins << lambda do |app|
|
42
|
+
CloudKit::Service.new(app, :collections => args.to_a)
|
43
|
+
end
|
44
|
+
@last_cloudkit_id = @ins.last.object_id
|
45
|
+
end
|
46
|
+
|
47
|
+
def welcome #:nodoc:
|
48
|
+
doc = <<HTML
|
49
|
+
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
|
50
|
+
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
51
|
+
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
|
52
|
+
<head>
|
53
|
+
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
|
54
|
+
<title>CloudKit via cURL</title>
|
55
|
+
<style type="text/css">
|
56
|
+
body {
|
57
|
+
font-family: 'Helvetica', 'Arial', san-serif;
|
58
|
+
font-size: 15px;
|
59
|
+
margin: 0;
|
60
|
+
padding: 0;
|
61
|
+
color: #222222;
|
62
|
+
}
|
63
|
+
h1 {
|
64
|
+
font-family: 'Helvetica Neue', 'Helvetica', 'Arial', san-serif;
|
65
|
+
font-size: 73px;
|
66
|
+
font-weight: bold;
|
67
|
+
line-height: 28px;
|
68
|
+
margin: 20px 0px 20px 0px;
|
69
|
+
}
|
70
|
+
.wrapper {
|
71
|
+
width: 500px;
|
72
|
+
margin: 0 auto;
|
73
|
+
clear: both;
|
74
|
+
}
|
75
|
+
p {
|
76
|
+
margin-top: 0px;
|
77
|
+
line-height: 1.5em;
|
78
|
+
}
|
79
|
+
#header {
|
80
|
+
background-color: #ffffcc;
|
81
|
+
display: block;
|
82
|
+
padding: 2px 0;
|
83
|
+
margin: 35px 0px 10px 0px;
|
84
|
+
border-top: 1px solid #ffcc66;
|
85
|
+
border-bottom: 1px solid #ffcc66;
|
86
|
+
}
|
87
|
+
a {
|
88
|
+
color: #6b8df2;
|
89
|
+
text-decoration: none;
|
90
|
+
}
|
91
|
+
.meta {
|
92
|
+
padding: 7px 7px 7px 7px;
|
93
|
+
background-color: #ffccff;
|
94
|
+
border-top: 1px solid #cc99ff;
|
95
|
+
border-bottom: 1px solid #cc99ff;
|
96
|
+
font-size: 14px;
|
97
|
+
display: block;
|
98
|
+
margin: 10px 0px 10px 0px;
|
99
|
+
}
|
100
|
+
</style>
|
101
|
+
</head>
|
102
|
+
<body>
|
103
|
+
<div id="header">
|
104
|
+
<div class="wrapper">
|
105
|
+
<h1>CloudKit</h1>
|
106
|
+
</div>
|
107
|
+
</div>
|
108
|
+
<div class="meta">
|
109
|
+
<p class="wrapper">
|
110
|
+
This page is appearing because you have not set up a default app in your
|
111
|
+
rackup file. To learn more about CloudKit, check out
|
112
|
+
<a href="http://getcloudkit.com">the site</a>.
|
113
|
+
</p>
|
114
|
+
</div>
|
115
|
+
</body>
|
116
|
+
</html>
|
117
|
+
HTML
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Rack #:nodoc:
|
2
|
+
|
3
|
+
# A minimal router providing just what is needed for the OAuth and OpenID
|
4
|
+
# filters.
|
5
|
+
class Router
|
6
|
+
|
7
|
+
# Create an instance of Router to match on method, path and params.
|
8
|
+
def initialize(method, path, params=[])
|
9
|
+
@method = method.to_s.upcase; @path = path; @params = params
|
10
|
+
end
|
11
|
+
|
12
|
+
# By overriding the case comparison operator, we can match routes in a case
|
13
|
+
# statement.
|
14
|
+
#
|
15
|
+
# See also: CloudKit::Util#r, CloudKit::Request#match?
|
16
|
+
def ===(request)
|
17
|
+
request.match?(@method, @path, @params)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,159 @@
|
|
1
|
+
module CloudKit
|
2
|
+
|
3
|
+
# A subclass of Rack::Request providing CloudKit-specific features.
|
4
|
+
class Request < Rack::Request
|
5
|
+
include CloudKit::Util
|
6
|
+
alias_method :cloudkit_params, :params
|
7
|
+
|
8
|
+
def initialize(env)
|
9
|
+
super(env)
|
10
|
+
end
|
11
|
+
|
12
|
+
# Return a merged set of both standard params and OAuth header params.
|
13
|
+
def params
|
14
|
+
@cloudkit_params ||= cloudkit_params.merge(oauth_header_params)
|
15
|
+
end
|
16
|
+
|
17
|
+
# Return true if method, path, and required_params match.
|
18
|
+
def match?(method, path, required_params=[])
|
19
|
+
(request_method == method) &&
|
20
|
+
path_info.match(path.gsub(':id', '*')) && # just enough to work for now
|
21
|
+
param_match?(required_params)
|
22
|
+
end
|
23
|
+
|
24
|
+
# Return true of the array of required params match the request params. If
|
25
|
+
# a hash in passed in for a param, its value is also used in the match.
|
26
|
+
def param_match?(required_params)
|
27
|
+
required_params.all? do |required_param|
|
28
|
+
case required_param
|
29
|
+
when Hash
|
30
|
+
key = required_param.keys.first
|
31
|
+
return false unless params.has_key? key
|
32
|
+
return false unless params[key] == required_param[key]
|
33
|
+
when String
|
34
|
+
return false unless params.has_key? required_param
|
35
|
+
else
|
36
|
+
false
|
37
|
+
end
|
38
|
+
true
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# Return OAuth header params in a hash.
|
43
|
+
def oauth_header_params
|
44
|
+
# This is a copy of the same method from the OAuth gem.
|
45
|
+
# TODO: Refactor the OAuth gem so that this method is available via a
|
46
|
+
# mixin, outside of the request proxy context.
|
47
|
+
%w( X-HTTP_AUTHORIZATION Authorization HTTP_AUTHORIZATION ).each do |header|
|
48
|
+
next unless @env.include?(header)
|
49
|
+
header = @env[header]
|
50
|
+
next unless header[0,6] == 'OAuth '
|
51
|
+
oauth_param_string = header[6,header.length].split(/[,=]/)
|
52
|
+
oauth_param_string.map!{|v| unescape(v.strip)}
|
53
|
+
oauth_param_string.map!{|v| v =~ /^\".*\"$/ ? v[1..-2] : v}
|
54
|
+
oauth_params = Hash[*oauth_param_string.flatten]
|
55
|
+
oauth_params.reject!{|k,v| k !~ /^oauth_/}
|
56
|
+
return oauth_params
|
57
|
+
end
|
58
|
+
return {}
|
59
|
+
end
|
60
|
+
|
61
|
+
# Unescape a value according to the OAuth spec.
|
62
|
+
def unescape(value)
|
63
|
+
URI.unescape(value.gsub('+', '%2B'))
|
64
|
+
end
|
65
|
+
|
66
|
+
# Return the last path element in the request URI.
|
67
|
+
def last_path_element
|
68
|
+
path_element(-1)
|
69
|
+
end
|
70
|
+
|
71
|
+
# Return a specific path element
|
72
|
+
def path_element(index)
|
73
|
+
path_info.split('/')[index] rescue nil
|
74
|
+
end
|
75
|
+
|
76
|
+
# Return an array containing one entry for each piece of upstream
|
77
|
+
# middleware. This is in the same spirit as Via headers in HTTP, but does
|
78
|
+
# not use the header because the transition from one piece of middleware to
|
79
|
+
# the next does not use HTTP.
|
80
|
+
def via
|
81
|
+
@env[via_key].split(', ') rescue []
|
82
|
+
end
|
83
|
+
|
84
|
+
# Return parsed contents of an If-Match header.
|
85
|
+
#
|
86
|
+
# Note: Only a single ETag is useful in the context of CloudKit, so a list
|
87
|
+
# is treated as one ETag; the result of using the wrong ETag or a list of
|
88
|
+
# ETags is the same in the context of PUT and DELETE where If-Match
|
89
|
+
# headers are required.
|
90
|
+
def if_match
|
91
|
+
etag = @env['HTTP_IF_MATCH']
|
92
|
+
return nil unless etag
|
93
|
+
etag.strip!
|
94
|
+
etag = unquote(etag)
|
95
|
+
return nil if etag == '*'
|
96
|
+
etag
|
97
|
+
end
|
98
|
+
|
99
|
+
# Add a via entry to the Rack environment.
|
100
|
+
def inject_via(key)
|
101
|
+
items = via << key
|
102
|
+
@env[via_key] = items.join(', ')
|
103
|
+
end
|
104
|
+
|
105
|
+
# Return the current user URI.
|
106
|
+
def current_user
|
107
|
+
return nil unless @env[auth_key] && @env[auth_key] != ''
|
108
|
+
@env[auth_key]
|
109
|
+
end
|
110
|
+
|
111
|
+
# Set the current user URI.
|
112
|
+
def current_user=(user)
|
113
|
+
@env[auth_key] = user
|
114
|
+
end
|
115
|
+
|
116
|
+
# Return true if authentication is being used.
|
117
|
+
def using_auth?
|
118
|
+
@env[auth_presence_key] != nil
|
119
|
+
end
|
120
|
+
|
121
|
+
# Report to downstream middleware that authentication is in use.
|
122
|
+
def announce_auth(via)
|
123
|
+
inject_via(via)
|
124
|
+
@env[auth_presence_key] = 1
|
125
|
+
end
|
126
|
+
|
127
|
+
# Return the session associated with this request.
|
128
|
+
def session
|
129
|
+
@env['rack.session']
|
130
|
+
end
|
131
|
+
|
132
|
+
# Return the login URL for this request. This is stashed in the Rack
|
133
|
+
# environment so the OpenID and OAuth middleware can cooperate during the
|
134
|
+
# token authorization step in the OAuth flow.
|
135
|
+
def login_url
|
136
|
+
@env[login_url_key] || '/login'
|
137
|
+
end
|
138
|
+
|
139
|
+
# Set the login url for this request.
|
140
|
+
def login_url=(url)
|
141
|
+
@env[login_url_key] = url
|
142
|
+
end
|
143
|
+
|
144
|
+
# Return the logout URL for this request.
|
145
|
+
def logout_url
|
146
|
+
@env[logout_url_key] || '/logout'
|
147
|
+
end
|
148
|
+
|
149
|
+
# Set the logout URL for this request.
|
150
|
+
def logout_url=(url)
|
151
|
+
@env[logout_url_key] = url
|
152
|
+
end
|
153
|
+
|
154
|
+
# Return the flash session for this request.
|
155
|
+
def flash
|
156
|
+
session[flash_key] ||= CloudKit::FlashSession.new
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|