ruby-openid 1.0
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.
- data/COPYING +21 -0
- data/INSTALL +34 -0
- data/README +67 -0
- data/TODO +9 -0
- data/examples/README +54 -0
- data/examples/cacert.pem +7815 -0
- data/examples/consumer.rb +285 -0
- data/examples/openid-store/associations/http-localhost_3A3000_2Fserver-EMQbAy3NnHVzA.s0u5KAcplKGzo +6 -0
- data/examples/openid-store/auth_key +1 -0
- data/examples/rails_active_record_store/README +59 -0
- data/examples/rails_active_record_store/XX_add_openidstore.rb +30 -0
- data/examples/rails_active_record_store/models/openid_association.rb +12 -0
- data/examples/rails_active_record_store/models/openid_nonce.rb +3 -0
- data/examples/rails_active_record_store/models/openid_setting.rb +2 -0
- data/examples/rails_active_record_store/openid_helper.rb +91 -0
- data/examples/rails_active_record_store/openidstore_test.rb +15 -0
- data/examples/rails_active_record_store/schema.mysql.sql +22 -0
- data/examples/rails_active_record_store/schema.postgresql.sql +21 -0
- data/examples/rails_active_record_store/schema.sqlite.sql +21 -0
- data/examples/rails_openid_login_generator/USAGE +23 -0
- data/examples/rails_openid_login_generator/openid_login_generator.rb +36 -0
- data/examples/rails_openid_login_generator/templates/README +116 -0
- data/examples/rails_openid_login_generator/templates/controller.rb +116 -0
- data/examples/rails_openid_login_generator/templates/controller_test.rb +0 -0
- data/examples/rails_openid_login_generator/templates/helper.rb +2 -0
- data/examples/rails_openid_login_generator/templates/openid_login_system.rb +87 -0
- data/examples/rails_openid_login_generator/templates/user.rb +14 -0
- data/examples/rails_openid_login_generator/templates/user_test.rb +0 -0
- data/examples/rails_openid_login_generator/templates/users.yml +0 -0
- data/examples/rails_openid_login_generator/templates/view_login.rhtml +15 -0
- data/examples/rails_openid_login_generator/templates/view_logout.rhtml +10 -0
- data/examples/rails_openid_login_generator/templates/view_welcome.rhtml +9 -0
- data/examples/rails_server/README +153 -0
- data/examples/rails_server/Rakefile +10 -0
- data/examples/rails_server/app/controllers/application.rb +4 -0
- data/examples/rails_server/app/controllers/login_controller.rb +35 -0
- data/examples/rails_server/app/controllers/server_controller.rb +185 -0
- data/examples/rails_server/app/helpers/application_helper.rb +3 -0
- data/examples/rails_server/app/helpers/login_helper.rb +2 -0
- data/examples/rails_server/app/helpers/server_helper.rb +9 -0
- data/examples/rails_server/app/views/layouts/server.rhtml +61 -0
- data/examples/rails_server/app/views/login/index.rhtml +32 -0
- data/examples/rails_server/app/views/server/decide.rhtml +11 -0
- data/examples/rails_server/config/boot.rb +19 -0
- data/examples/rails_server/config/database.yml +85 -0
- data/examples/rails_server/config/environment.rb +53 -0
- data/examples/rails_server/config/environments/development.rb +19 -0
- data/examples/rails_server/config/environments/production.rb +19 -0
- data/examples/rails_server/config/environments/test.rb +19 -0
- data/examples/rails_server/config/routes.rb +23 -0
- data/examples/rails_server/db/openid-store/associations/http-localhost_2F_7Cnormal-YU.tkND1J4fEZhnuAoT5Zc0yCA0 +6 -0
- data/examples/rails_server/doc/README_FOR_APP +2 -0
- data/examples/rails_server/log/development.log +6059 -0
- data/examples/rails_server/log/production.log +0 -0
- data/examples/rails_server/log/server.log +0 -0
- data/examples/rails_server/log/test.log +0 -0
- data/examples/rails_server/public/404.html +8 -0
- data/examples/rails_server/public/500.html +8 -0
- data/examples/rails_server/public/dispatch.cgi +12 -0
- data/examples/rails_server/public/dispatch.fcgi +26 -0
- data/examples/rails_server/public/dispatch.rb +12 -0
- data/examples/rails_server/public/favicon.ico +0 -0
- data/examples/rails_server/public/images/rails.png +0 -0
- data/examples/rails_server/public/javascripts/controls.js +750 -0
- data/examples/rails_server/public/javascripts/dragdrop.js +584 -0
- data/examples/rails_server/public/javascripts/effects.js +854 -0
- data/examples/rails_server/public/javascripts/prototype.js +1785 -0
- data/examples/rails_server/public/robots.txt +1 -0
- data/examples/rails_server/script/about +3 -0
- data/examples/rails_server/script/breakpointer +3 -0
- data/examples/rails_server/script/console +3 -0
- data/examples/rails_server/script/destroy +3 -0
- data/examples/rails_server/script/generate +3 -0
- data/examples/rails_server/script/performance/benchmarker +3 -0
- data/examples/rails_server/script/performance/profiler +3 -0
- data/examples/rails_server/script/plugin +3 -0
- data/examples/rails_server/script/process/reaper +3 -0
- data/examples/rails_server/script/process/spawner +3 -0
- data/examples/rails_server/script/process/spinner +3 -0
- data/examples/rails_server/script/runner +3 -0
- data/examples/rails_server/script/server +3 -0
- data/examples/rails_server/test/functional/login_controller_test.rb +18 -0
- data/examples/rails_server/test/functional/server_controller_test.rb +18 -0
- data/examples/rails_server/test/test_helper.rb +28 -0
- data/lib/hmac-md5.rb +11 -0
- data/lib/hmac-rmd160.rb +11 -0
- data/lib/hmac-sha1.rb +11 -0
- data/lib/hmac-sha2.rb +25 -0
- data/lib/hmac.rb +112 -0
- data/lib/openid/association.rb +109 -0
- data/lib/openid/consumer.rb +928 -0
- data/lib/openid/dh.rb +48 -0
- data/lib/openid/discovery.rb +89 -0
- data/lib/openid/fetchers.rb +119 -0
- data/lib/openid/filestore.rb +315 -0
- data/lib/openid/htmltokenizer.rb +355 -0
- data/lib/openid/parse.rb +23 -0
- data/lib/openid/server.rb +951 -0
- data/lib/openid/service.rb +135 -0
- data/lib/openid/stores.rb +178 -0
- data/lib/openid/trustroot.rb +100 -0
- data/lib/openid/util.rb +273 -0
- data/test/assoc.rb +38 -0
- data/test/consumer.rb +384 -0
- data/test/dh.rb +20 -0
- data/test/extensions.rb +30 -0
- data/test/linkparse.rb +305 -0
- data/test/runtests.rb +11 -0
- data/test/server2.rb +1053 -0
- data/test/storetestcase.rb +172 -0
- data/test/teststore.rb +23 -0
- data/test/trustroot.rb +113 -0
- data/test/util.rb +56 -0
- metadata +218 -0
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
# = HTMLTokenizer
|
|
2
|
+
#
|
|
3
|
+
# Author:: Ben Giddings (mailto:bg-rubyforge@infofiend.com)
|
|
4
|
+
# Copyright:: Copyright (c) 2004 Ben Giddings
|
|
5
|
+
# License:: Distributes under the same terms as Ruby
|
|
6
|
+
#
|
|
7
|
+
#
|
|
8
|
+
# This is a partial port of the functionality behind Perl's TokeParser
|
|
9
|
+
# Provided a page it progressively returns tokens from that page
|
|
10
|
+
#
|
|
11
|
+
# $Id: htmltokenizer.rb,v 1.7 2005/06/07 21:05:53 merc Exp $
|
|
12
|
+
|
|
13
|
+
#
|
|
14
|
+
# A class to tokenize HTML.
|
|
15
|
+
#
|
|
16
|
+
# Example:
|
|
17
|
+
#
|
|
18
|
+
# page = "<HTML>
|
|
19
|
+
# <HEAD>
|
|
20
|
+
# <TITLE>This is the title</TITLE>
|
|
21
|
+
# </HEAD>
|
|
22
|
+
# <!-- Here comes the <a href=\"missing.link\">blah</a>
|
|
23
|
+
# comment body
|
|
24
|
+
# -->
|
|
25
|
+
# <BODY>
|
|
26
|
+
# <H1>This is the header</H1>
|
|
27
|
+
# <P>
|
|
28
|
+
# This is the paragraph, it contains
|
|
29
|
+
# <a href=\"link.html\">links</a>,
|
|
30
|
+
# <img src=\"blah.gif\" optional alt='images
|
|
31
|
+
# are
|
|
32
|
+
# really cool'>. Ok, here is some more text and
|
|
33
|
+
# <A href=\"http://another.link.com/\" target=\"_blank\">another link</A>.
|
|
34
|
+
# </P>
|
|
35
|
+
# </body>
|
|
36
|
+
# </HTML>
|
|
37
|
+
# "
|
|
38
|
+
# toke = HTMLTokenizer.new(page)
|
|
39
|
+
#
|
|
40
|
+
# assert("<h1>" == toke.getTag("h1", "h2", "h3").to_s.downcase)
|
|
41
|
+
# assert(HTMLTag.new("<a href=\"link.html\">") == toke.getTag("IMG", "A"))
|
|
42
|
+
# assert("links" == toke.getTrimmedText)
|
|
43
|
+
# assert(toke.getTag("IMG", "A").attr_hash['optional'])
|
|
44
|
+
# assert("_blank" == toke.getTag("IMG", "A").attr_hash['target'])
|
|
45
|
+
#
|
|
46
|
+
class HTMLTokenizer
|
|
47
|
+
@@version = 1.0
|
|
48
|
+
|
|
49
|
+
# Get version of HTMLTokenizer lib
|
|
50
|
+
def self.version
|
|
51
|
+
@@version
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
attr_reader :page
|
|
55
|
+
|
|
56
|
+
# Create a new tokenizer, based on the content, used as a string.
|
|
57
|
+
def initialize(content)
|
|
58
|
+
@page = content.to_s
|
|
59
|
+
@cur_pos = 0
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Reset the parser, setting the current position back at the stop
|
|
63
|
+
def reset
|
|
64
|
+
@cur_pos = 0
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Look at the next token, but don't actually grab it
|
|
68
|
+
def peekNextToken
|
|
69
|
+
if @cur_pos == @page.length then return nil end
|
|
70
|
+
|
|
71
|
+
if ?< == @page[@cur_pos]
|
|
72
|
+
# Next token is a tag of some kind
|
|
73
|
+
if '!--' == @page[(@cur_pos + 1), 3]
|
|
74
|
+
# Token is a comment
|
|
75
|
+
tag_end = @page.index('-->', (@cur_pos + 1))
|
|
76
|
+
if tag_end.nil?
|
|
77
|
+
raise "No end found to started comment:\n#{@page[@cur_pos,80]}"
|
|
78
|
+
end
|
|
79
|
+
# p @page[@cur_pos .. (tag_end+2)]
|
|
80
|
+
HTMLComment.new(@page[@cur_pos .. (tag_end + 2)])
|
|
81
|
+
else
|
|
82
|
+
# Token is a html tag
|
|
83
|
+
tag_end = @page.index('>', (@cur_pos + 1))
|
|
84
|
+
if tag_end.nil?
|
|
85
|
+
raise "No end found to started tag:\n#{@page[@cur_pos,80]}"
|
|
86
|
+
end
|
|
87
|
+
# p @page[@cur_pos .. tag_end]
|
|
88
|
+
HTMLTag.new(@page[@cur_pos .. tag_end])
|
|
89
|
+
end
|
|
90
|
+
else
|
|
91
|
+
# Next token is text
|
|
92
|
+
text_end = @page.index('<', @cur_pos)
|
|
93
|
+
text_end = text_end.nil? ? -1 : (text_end - 1)
|
|
94
|
+
# p @page[@cur_pos .. text_end]
|
|
95
|
+
HTMLText.new(@page[@cur_pos .. text_end])
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Get the next token, returns an instance of
|
|
100
|
+
# * HTMLText
|
|
101
|
+
# * HTMLToken
|
|
102
|
+
# * HTMLTag
|
|
103
|
+
def getNextToken
|
|
104
|
+
token = peekNextToken
|
|
105
|
+
if token
|
|
106
|
+
# @page = @page[token.raw.length .. -1]
|
|
107
|
+
# @page.slice!(0, token.raw.length)
|
|
108
|
+
@cur_pos += token.raw.length
|
|
109
|
+
end
|
|
110
|
+
#p token
|
|
111
|
+
#print token.raw
|
|
112
|
+
return token
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Get a tag from the specified set of desired tags.
|
|
116
|
+
# For example:
|
|
117
|
+
# <tt>foo = toke.getTag("h1", "h2", "h3")</tt>
|
|
118
|
+
# Will return the next header tag encountered.
|
|
119
|
+
def getTag(*sought_tags)
|
|
120
|
+
sought_tags.collect! {|elm| elm.downcase}
|
|
121
|
+
|
|
122
|
+
while (tag = getNextToken)
|
|
123
|
+
if tag.kind_of?(HTMLTag) and
|
|
124
|
+
(0 == sought_tags.length or sought_tags.include?(tag.tag_name))
|
|
125
|
+
break
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
tag
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Get all the text between the current position and the next tag
|
|
132
|
+
# (if specified) or a specific later tag
|
|
133
|
+
def getText(until_tag = nil)
|
|
134
|
+
if until_tag.nil?
|
|
135
|
+
if ?< == @page[@cur_pos]
|
|
136
|
+
# Next token is a tag, not text
|
|
137
|
+
""
|
|
138
|
+
else
|
|
139
|
+
# Next token is text
|
|
140
|
+
getNextToken.text
|
|
141
|
+
end
|
|
142
|
+
else
|
|
143
|
+
ret_str = ""
|
|
144
|
+
|
|
145
|
+
while (tag = peekNextToken)
|
|
146
|
+
if tag.kind_of?(HTMLTag) and tag.tag_name == until_tag
|
|
147
|
+
break
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
if ("" != tag.text)
|
|
151
|
+
ret_str << (tag.text + " ")
|
|
152
|
+
end
|
|
153
|
+
getNextToken
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
ret_str
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Like getText, but squeeze all whitespace, getting rid of
|
|
161
|
+
# leading and trailing whitespace, and squeezing multiple
|
|
162
|
+
# spaces into a single space.
|
|
163
|
+
def getTrimmedText(until_tag = nil)
|
|
164
|
+
getText(until_tag).strip.gsub(/\s+/m, " ")
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# The parent class for all three types of HTML tokens
|
|
170
|
+
class HTMLToken
|
|
171
|
+
attr_accessor :raw
|
|
172
|
+
|
|
173
|
+
# Initialize the token based on the raw text
|
|
174
|
+
def initialize(text)
|
|
175
|
+
@raw = text
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# By default, return exactly the string used to create the text
|
|
179
|
+
def to_s
|
|
180
|
+
raw
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# By default tokens have no text representation
|
|
184
|
+
def text
|
|
185
|
+
""
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def trimmed_text
|
|
189
|
+
text.strip.gsub(/\s+/m, " ")
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Compare to another based on the raw source
|
|
193
|
+
def ==(other)
|
|
194
|
+
raw == other.to_s
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# Class representing text that isn't inside a tag
|
|
199
|
+
class HTMLText < HTMLToken
|
|
200
|
+
def text
|
|
201
|
+
raw
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# Class representing an HTML comment
|
|
206
|
+
class HTMLComment < HTMLToken
|
|
207
|
+
attr_accessor :contents
|
|
208
|
+
def initialize(text)
|
|
209
|
+
super(text)
|
|
210
|
+
temp_arr = text.scan(/^<!--\s*(.*?)\s*-->$/m)
|
|
211
|
+
if temp_arr[0].nil?
|
|
212
|
+
raise "Text passed to HTMLComment.initialize is not a comment"
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
@contents = temp_arr[0][0]
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# Class representing an HTML tag
|
|
220
|
+
class HTMLTag < HTMLToken
|
|
221
|
+
attr_reader :end_tag, :tag_name
|
|
222
|
+
def initialize(text)
|
|
223
|
+
super(text)
|
|
224
|
+
if ?< != text[0] or ?> != text[-1]
|
|
225
|
+
raise "Text passed to HTMLComment.initialize is not a comment"
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
@attr_hash = Hash.new
|
|
229
|
+
@raw = text
|
|
230
|
+
|
|
231
|
+
tag_name = text.scan(/[\w:-]+/)[0]
|
|
232
|
+
if tag_name.nil?
|
|
233
|
+
raise "Error, tag is nil: #{tag_name}"
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
if ?/ == text[1]
|
|
237
|
+
# It's an end tag
|
|
238
|
+
@end_tag = true
|
|
239
|
+
@tag_name = '/' + tag_name.downcase
|
|
240
|
+
else
|
|
241
|
+
@end_tag = false
|
|
242
|
+
@tag_name = tag_name.downcase
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
@hashed = false
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
# Retrieve a hash of all the tag's attributes.
|
|
249
|
+
# Lazily done, so that if you don't look at a tag's attributes
|
|
250
|
+
# things go quicker
|
|
251
|
+
def attr_hash
|
|
252
|
+
# Lazy initialize == don't build the hash until it's needed
|
|
253
|
+
if !@hashed
|
|
254
|
+
if !@end_tag
|
|
255
|
+
# Get the attributes
|
|
256
|
+
attr_arr = @raw.scan(/<[\w:-]+\s+(.*)>/m)[0]
|
|
257
|
+
if attr_arr.kind_of?(Array)
|
|
258
|
+
# Attributes found, parse them
|
|
259
|
+
attrs = attr_arr[0]
|
|
260
|
+
attr_arr = attrs.scan(/\s*([\w:-]+)(?:\s*=\s*("[^"]*"|'[^']*'|([^"'>][^\s>]*)))?/m)
|
|
261
|
+
# clean up the array by:
|
|
262
|
+
# * setting all nil elements to true
|
|
263
|
+
# * removing enclosing quotes
|
|
264
|
+
attr_arr.each {
|
|
265
|
+
|item|
|
|
266
|
+
val = if item[1].nil?
|
|
267
|
+
item[0]
|
|
268
|
+
elsif '"'[0] == item[1][0] or '\''[0] == item[1][0]
|
|
269
|
+
item[1][1 .. -2]
|
|
270
|
+
else
|
|
271
|
+
item[1]
|
|
272
|
+
end
|
|
273
|
+
@attr_hash[item[0].downcase] = val
|
|
274
|
+
}
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
@hashed = true
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
#p self
|
|
281
|
+
|
|
282
|
+
@attr_hash
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
# Get the 'alt' text for a tag, if it exists, or an empty string otherwise
|
|
286
|
+
def text
|
|
287
|
+
if !end_tag
|
|
288
|
+
case tag_name
|
|
289
|
+
when 'img'
|
|
290
|
+
if !attr_hash['alt'].nil?
|
|
291
|
+
return attr_hash['alt']
|
|
292
|
+
end
|
|
293
|
+
when 'applet'
|
|
294
|
+
if !attr_hash['alt'].nil?
|
|
295
|
+
return attr_hash['alt']
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
return ''
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
if $0 == __FILE__
|
|
304
|
+
require 'test/unit'
|
|
305
|
+
|
|
306
|
+
class TC_TestHTMLTokenizer < Test::Unit::TestCase
|
|
307
|
+
def test_bad_link
|
|
308
|
+
toke = HTMLTokenizer.new("<p><a href=http://bad.com/link>foo</a></p>")
|
|
309
|
+
assert("http://bad.com/link" == toke.getTag("a").attr_hash['href'])
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
def test_namespace
|
|
313
|
+
toke = HTMLTokenizer.new("<f:table xmlns:f=\"http://www.com/foo\">")
|
|
314
|
+
assert("http://www.com/foo" == toke.getTag("f:table").attr_hash['xmlns:f'])
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
def test_comment
|
|
318
|
+
toke = HTMLTokenizer.new("<!-- comment on me -->")
|
|
319
|
+
t = toke.getNextToken
|
|
320
|
+
assert(HTMLComment == t.class)
|
|
321
|
+
assert("comment on me" == t.contents)
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def test_full
|
|
326
|
+
page = "<HTML>
|
|
327
|
+
<HEAD>
|
|
328
|
+
<TITLE>This is the title</TITLE>
|
|
329
|
+
</HEAD>
|
|
330
|
+
<!-- Here comes the <a href=\"missing.link\">blah</a>
|
|
331
|
+
comment body
|
|
332
|
+
-->
|
|
333
|
+
<BODY>
|
|
334
|
+
<H1>This is the header</H1>
|
|
335
|
+
<P>
|
|
336
|
+
This is the paragraph, it contains
|
|
337
|
+
<a href=\"link.html\">links</a>,
|
|
338
|
+
<img src=\"blah.gif\" optional alt='images
|
|
339
|
+
are
|
|
340
|
+
really cool'>. Ok, here is some more text and
|
|
341
|
+
<A href=\"http://another.link.com/\" target=\"_blank\">another link</A>.
|
|
342
|
+
</P>
|
|
343
|
+
</body>
|
|
344
|
+
</HTML>
|
|
345
|
+
"
|
|
346
|
+
toke = HTMLTokenizer.new(page)
|
|
347
|
+
|
|
348
|
+
assert("<h1>" == toke.getTag("h1", "h2", "h3").to_s.downcase)
|
|
349
|
+
assert(HTMLTag.new("<a href=\"link.html\">") == toke.getTag("IMG", "A"))
|
|
350
|
+
assert("links" == toke.getTrimmedText)
|
|
351
|
+
assert(toke.getTag("IMG", "A").attr_hash['optional'])
|
|
352
|
+
assert("_blank" == toke.getTag("IMG", "A").attr_hash['target'])
|
|
353
|
+
end
|
|
354
|
+
end
|
|
355
|
+
end
|
data/lib/openid/parse.rb
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
require "openid/htmltokenizer"
|
|
2
|
+
|
|
3
|
+
def parse_link_attrs(data)
|
|
4
|
+
parser = HTMLTokenizer.new(data)
|
|
5
|
+
in_head = false
|
|
6
|
+
begin
|
|
7
|
+
while el = parser.getTag("head", "link", "body")
|
|
8
|
+
if el.tag_name == "head"
|
|
9
|
+
in_head = true
|
|
10
|
+
elsif el.tag_name == "link"
|
|
11
|
+
continue unless in_head
|
|
12
|
+
yield el.attr_hash
|
|
13
|
+
elsif el.tag_name == "body"
|
|
14
|
+
return
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
rescue
|
|
18
|
+
return
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
|
|
@@ -0,0 +1,951 @@
|
|
|
1
|
+
require 'openid/util'
|
|
2
|
+
require 'openid/association'
|
|
3
|
+
require 'openid/dh'
|
|
4
|
+
require 'openid/trustroot'
|
|
5
|
+
|
|
6
|
+
module OpenID
|
|
7
|
+
|
|
8
|
+
# This module contains classes specific to implemeting an OpenID
|
|
9
|
+
# server.
|
|
10
|
+
#
|
|
11
|
+
# ==Overview
|
|
12
|
+
#
|
|
13
|
+
# An OpenID server must perform three tasks:
|
|
14
|
+
#
|
|
15
|
+
# 1. Examine the incoming request to determine its nature and validity.
|
|
16
|
+
#
|
|
17
|
+
# 2. Make a decision about how to respond to this request.
|
|
18
|
+
#
|
|
19
|
+
# 3. Format the response according to the protocol.
|
|
20
|
+
#
|
|
21
|
+
# The first and last of these tasks may performed by
|
|
22
|
+
# the Server.decode_request and Server.encode_response methods of the
|
|
23
|
+
# OpenID::Server::Server object. Who gets to do the intermediate task --
|
|
24
|
+
# deciding how to respond to the request -- will depend on what type of
|
|
25
|
+
# request it is.
|
|
26
|
+
#
|
|
27
|
+
# If it's a request to authenticate a user (a checkid_setup or
|
|
28
|
+
# checkid_immediate request), you need to decide if you will assert
|
|
29
|
+
# that this user may claim the identity in question. Exactly how you do
|
|
30
|
+
# that is a matter of application policy, but it generally involves making
|
|
31
|
+
# sure the user has an account with your system and is logged in, checking
|
|
32
|
+
# to see if that identity is hers to claim, and verifying with the user that
|
|
33
|
+
# she does consent to releasing that information to the party making the
|
|
34
|
+
# request. Do this by examining the properties of the CheckIDRequest
|
|
35
|
+
# object, and when you've come to a decision, form a response by calling
|
|
36
|
+
# CheckIDRequest.answer.
|
|
37
|
+
#
|
|
38
|
+
# Other types of requests relate to establishing associations between client
|
|
39
|
+
# and server and verifing the authenticity of previous communications.
|
|
40
|
+
# Server contains all the logic and data necessary to respond to
|
|
41
|
+
# such requests; just pass it to Server.handle_request.
|
|
42
|
+
#
|
|
43
|
+
#
|
|
44
|
+
# ==OpenID Extensions
|
|
45
|
+
#
|
|
46
|
+
# Do you want to provide other information for your users
|
|
47
|
+
# in addition to authentication? Version 1.2 of the OpenID
|
|
48
|
+
# protocol allows consumers to add extensions to their requests.
|
|
49
|
+
# For example, with sites using the Simple Registration Extension
|
|
50
|
+
# http://www.openidenabled.com/openid/simple-registration-extension/,
|
|
51
|
+
# a user can agree to have their nickname and e-mail address sent to a
|
|
52
|
+
# site when they sign up.
|
|
53
|
+
#
|
|
54
|
+
# Since extensions do not change the way OpenID authentication works,
|
|
55
|
+
# code to handle extension requests may be completely separate from the
|
|
56
|
+
# OpenIDRequest class here. But you'll likely want data sent back by
|
|
57
|
+
# your extension to be signed. OpenIDResponse provides methods with
|
|
58
|
+
# which you can add data to it which can be signed with the other data in
|
|
59
|
+
# the OpenID signature.
|
|
60
|
+
#
|
|
61
|
+
# For example when request is a checkid_* request:
|
|
62
|
+
#
|
|
63
|
+
# response = request.answer(true)
|
|
64
|
+
# # this will add a signed 'openid.sreg.timezone' parameter to the response
|
|
65
|
+
# response.add_field('sreg', 'timezone', 'America/Los_Angeles')
|
|
66
|
+
#
|
|
67
|
+
#
|
|
68
|
+
# ==Stores
|
|
69
|
+
#
|
|
70
|
+
# The OpenID server needs to maintain state between requests in order
|
|
71
|
+
# to function. Its mechanism for doing this is called a store. The
|
|
72
|
+
# store interface is defined in OpenID::Store.
|
|
73
|
+
# Additionally, several concrete store implementations are provided, so that
|
|
74
|
+
# most sites won't need to implement a custom store. For a store backed
|
|
75
|
+
# by flat files on disk, see OpenID::FilesystemStore.
|
|
76
|
+
#
|
|
77
|
+
# ==Upgrading
|
|
78
|
+
#
|
|
79
|
+
# The keys by which a server looks up associations in its store have changed
|
|
80
|
+
# in version 1.2 of this library. If your store has entries created from
|
|
81
|
+
# version 1.0 code, you should empty it.
|
|
82
|
+
module Server
|
|
83
|
+
|
|
84
|
+
HTTP_REDIRECT = 302
|
|
85
|
+
HTTP_OK = 200
|
|
86
|
+
HTTP_ERROR = 400
|
|
87
|
+
|
|
88
|
+
BROWSER_REQUEST_MODES = ['checkid_setup', 'checkid_immediate']
|
|
89
|
+
OPENID_PREFIX = 'openid.'
|
|
90
|
+
|
|
91
|
+
ENCODE_KVFORM = ['kvform'].freeze
|
|
92
|
+
ENCODE_URL = ['URL/redirect'].freeze
|
|
93
|
+
|
|
94
|
+
# Represents an incoming OpenID request.
|
|
95
|
+
class OpenIDRequest
|
|
96
|
+
|
|
97
|
+
attr_reader :mode
|
|
98
|
+
|
|
99
|
+
# +mode+ is the corresponsing "openid.mode" value of the request,
|
|
100
|
+
# and is available through the +mode+ method.
|
|
101
|
+
def initialize(mode)
|
|
102
|
+
@mode = mode
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# A request to verify the validity of a previous response.
|
|
108
|
+
class CheckAuthRequest < OpenIDRequest
|
|
109
|
+
|
|
110
|
+
attr_accessor :assoc_handle, :sig, :signed, :invalidate_handle
|
|
111
|
+
|
|
112
|
+
# Create a new CheckAuthRequest. Please see the
|
|
113
|
+
# CheckAuthRequest.from_query class method, as it is the preferred
|
|
114
|
+
# way to produce this object.
|
|
115
|
+
#
|
|
116
|
+
# ==Parameters
|
|
117
|
+
#
|
|
118
|
+
# [+assoc_handle+]
|
|
119
|
+
# String value of the "openid.assoc_handle" from the incoming request
|
|
120
|
+
#
|
|
121
|
+
# [+sig+]
|
|
122
|
+
# String signature for the request. This is "openid.sig"
|
|
123
|
+
#
|
|
124
|
+
# [+signed+]
|
|
125
|
+
# An array of pairs of [key, value], where key is the signed paramter
|
|
126
|
+
# name without the "openid." prefix. Value is the String value of
|
|
127
|
+
# of the paramter.
|
|
128
|
+
#
|
|
129
|
+
# [+invalidate_handle+]
|
|
130
|
+
# Optional. The association handle that the consumer is asking about
|
|
131
|
+
# the validity of. May be nil.
|
|
132
|
+
def initialize(assoc_handle, sig, signed, invalidate_handle=nil)
|
|
133
|
+
super('check_authentication')
|
|
134
|
+
@assoc_handle = assoc_handle
|
|
135
|
+
@sig = sig
|
|
136
|
+
@signed = signed
|
|
137
|
+
@invalidate_handle = invalidate_handle
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Produce a CheckAuthRequest instance from the request query. Raises
|
|
141
|
+
# a ProtocolError if the request is malformed, and returns an instance
|
|
142
|
+
# CheckAuthRequest on success.
|
|
143
|
+
def CheckAuthRequest.from_query(query)
|
|
144
|
+
assoc_handle = query['openid.assoc_handle']
|
|
145
|
+
sig = query['openid.sig']
|
|
146
|
+
signed = query['openid.signed']
|
|
147
|
+
invalidate_handle = query['openid.invalidate_handle']
|
|
148
|
+
|
|
149
|
+
unless assoc_handle and sig and signed
|
|
150
|
+
raise ProtocolError.new(query,
|
|
151
|
+
"#{@mode} request missing required paramter")
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
signed = signed.split(',')
|
|
155
|
+
signed_pairs = []
|
|
156
|
+
|
|
157
|
+
signed.each do |field|
|
|
158
|
+
if field == 'mode'
|
|
159
|
+
value = 'id_res'
|
|
160
|
+
else
|
|
161
|
+
value = query['openid.'+field]
|
|
162
|
+
if value.nil?
|
|
163
|
+
raise ProtocolError.new(query, "Couldn't find signed field #{field}")
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
signed_pairs << [field, value]
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
return new(assoc_handle, sig, signed_pairs, invalidate_handle)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Respond to this request. Given an OpenID::Server::Signatory instance
|
|
173
|
+
# the validity of the signature and invalidate_handle can be checked.
|
|
174
|
+
#
|
|
175
|
+
# Returns an OpenIDResponse object with an appropriate "is_valid" field
|
|
176
|
+
# and an "invalidate_handle" field if appropriate.
|
|
177
|
+
def answer(signatory)
|
|
178
|
+
is_valid = signatory.verify(@assoc_handle, @sig, @signed)
|
|
179
|
+
signatory.invalidate(assoc_handle, true)
|
|
180
|
+
|
|
181
|
+
response = OpenIDResponse.new(self)
|
|
182
|
+
response.fields['is_valid'] = is_valid ? 'true' : 'false'
|
|
183
|
+
|
|
184
|
+
if @invalidate_handle
|
|
185
|
+
assoc = signatory.get_association(@invalidate_handle, false)
|
|
186
|
+
unless assoc
|
|
187
|
+
response.fields['invalidate_handle'] = @invalidate_handle
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
return response
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# An object that knows how to handle association requests with no
|
|
197
|
+
# session type.
|
|
198
|
+
class PlainTextServerSession
|
|
199
|
+
|
|
200
|
+
attr_reader :session_type
|
|
201
|
+
|
|
202
|
+
def initialize
|
|
203
|
+
@session_type = 'plaintext'
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def PlainTextServerSession.from_query(query)
|
|
207
|
+
new
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def answer(secret)
|
|
211
|
+
return {'mac_key' => OpenID::Util.to_base64(secret)}
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# An object that knows how to handle Diffie Hellman (DH-SHA1) association
|
|
217
|
+
# requests.
|
|
218
|
+
class DiffieHellmanServerSession
|
|
219
|
+
|
|
220
|
+
attr_reader :session_type, :dh, :consumer_pubkey
|
|
221
|
+
|
|
222
|
+
# In general you should create instances of DiffieHellmanServerSession
|
|
223
|
+
# with the from_query class method.
|
|
224
|
+
#
|
|
225
|
+
# ==Parameters
|
|
226
|
+
#
|
|
227
|
+
# [+dh+]
|
|
228
|
+
# OpenID::DiffieHellman instance
|
|
229
|
+
#
|
|
230
|
+
# [+consumer_pubkey+]
|
|
231
|
+
# Decoded "openid.dh_consumer_public".
|
|
232
|
+
def initialize(dh, consumer_pubkey)
|
|
233
|
+
@dh = dh
|
|
234
|
+
@consumer_pubkey = consumer_pubkey
|
|
235
|
+
@session_type = 'DH-SHA1'
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
# Creates a new DiffieHellmanServerSession from the incoming request.
|
|
239
|
+
# Raises a ProtocolError for malformed requests, and returns a
|
|
240
|
+
# DiffieHellmanServerSession instnace on success.
|
|
241
|
+
def DiffieHellmanServerSession.from_query(query)
|
|
242
|
+
dh_modulus = query['openid.dh_modulus']
|
|
243
|
+
dh_gen = query['openid.dh_gen']
|
|
244
|
+
dh = OpenID::DiffieHellman.from_base64(dh_modulus, dh_gen)
|
|
245
|
+
|
|
246
|
+
consumer_pubkey = query['openid.dh_consumer_public']
|
|
247
|
+
unless consumer_pubkey
|
|
248
|
+
msg = 'No openid.dh_consumer_public found for DH-SHA1 session'
|
|
249
|
+
raise ProtocolError.new(query, msg)
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
consumer_pubkey = OpenID::Util.base64_to_num(consumer_pubkey)
|
|
253
|
+
return new(dh, consumer_pubkey)
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
# Generate the arguments to be added to the response using +secret+.
|
|
257
|
+
def answer(secret)
|
|
258
|
+
mac_key = @dh.xor_secrect(@consumer_pubkey, secret)
|
|
259
|
+
return {
|
|
260
|
+
'dh_server_public' => OpenID::Util.num_to_base64(@dh.public),
|
|
261
|
+
'enc_mac_key' => OpenID::Util.to_base64(mac_key)
|
|
262
|
+
}
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
# A request to establish an OpenID association. This object contains the
|
|
268
|
+
# logic for handling "openid.mode=associate" requests.
|
|
269
|
+
class AssociateRequest < OpenIDRequest
|
|
270
|
+
|
|
271
|
+
attr_accessor :assoc_type, :session
|
|
272
|
+
|
|
273
|
+
# +session+ is an instance of PlainTextServerSession or
|
|
274
|
+
# DiffieHellmanServerSession
|
|
275
|
+
def initialize(session)
|
|
276
|
+
super('associate')
|
|
277
|
+
@assoc_type = 'HMAC-SHA1'
|
|
278
|
+
@session = session
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
# Generate an AssociateRequest instance from the incoming query.
|
|
282
|
+
# Raises a ProtocolError on malformed requests.
|
|
283
|
+
def AssociateRequest.from_query(query)
|
|
284
|
+
session_type = query['openid.session_type']
|
|
285
|
+
if session_type == 'DH-SHA1'
|
|
286
|
+
session = DiffieHellmanServerSession.from_query(query)
|
|
287
|
+
elsif session_type.nil?
|
|
288
|
+
session = PlainTextServerSession.from_query(query)
|
|
289
|
+
else
|
|
290
|
+
raise ProtocolError.new(query,
|
|
291
|
+
"Unknown session_type #{session_type}")
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
return new(session)
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
# Respond to this request with an association. +assoc+ is an
|
|
298
|
+
# OpenID::Association instance that represents the association to
|
|
299
|
+
# respond with.
|
|
300
|
+
def answer(assoc)
|
|
301
|
+
response = OpenIDResponse.new(self)
|
|
302
|
+
|
|
303
|
+
fields = {
|
|
304
|
+
'expires_in' => assoc.expires_in.to_s,
|
|
305
|
+
'assoc_type' => 'HMAC-SHA1',
|
|
306
|
+
'assoc_handle' => assoc.handle
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
# add the session specific arguments to the response fields
|
|
310
|
+
response.fields.update(fields)
|
|
311
|
+
response.fields.update(@session.answer(assoc.secret))
|
|
312
|
+
|
|
313
|
+
if @session.session_type != 'plaintext'
|
|
314
|
+
response.fields['session_type'] = @session.session_type
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
return response
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
# Object representing a request to confirm the identity of a
|
|
323
|
+
# user. This object handles requests for openid.mode checkid_immediate, and
|
|
324
|
+
# checkid_setup.
|
|
325
|
+
class CheckIDRequest < OpenIDRequest
|
|
326
|
+
|
|
327
|
+
attr_accessor :identity, :return_to, :trust_root, :immediate, \
|
|
328
|
+
:assoc_handle, :mode, :query
|
|
329
|
+
|
|
330
|
+
# Create a new CheckIDRequest instance. Most likely you'll want to
|
|
331
|
+
# use CheckIDRequest.from_query to create this object.
|
|
332
|
+
def initialize(mode, identity, return_to,
|
|
333
|
+
trust_root=nil, assoc_handle=nil)
|
|
334
|
+
|
|
335
|
+
unless ['checkid_immediate', 'checkid_setup'].include?(mode)
|
|
336
|
+
raise ProtocolError, "Can't create CheckIDRequest for mode #{mode}"
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
super(mode)
|
|
340
|
+
@identity = identity
|
|
341
|
+
@return_to = return_to
|
|
342
|
+
@trust_root = trust_root
|
|
343
|
+
@immediate = mode == 'checkid_immediate' ? true : false
|
|
344
|
+
@assoc_handle = assoc_handle
|
|
345
|
+
@query = {}
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
# Create a CheckIDRequest object from a web query. May raise a
|
|
350
|
+
# ProtocolError if request is a malformed checkid_* reuquest.
|
|
351
|
+
def CheckIDRequest.from_query(query)
|
|
352
|
+
mode = query['openid.mode']
|
|
353
|
+
|
|
354
|
+
identity = query['openid.identity']
|
|
355
|
+
raise ProtocolError.new(query, 'openid.identity missing') unless identity
|
|
356
|
+
return_to = query['openid.return_to']
|
|
357
|
+
raise ProtocolError.new(query, 'openid.return_to missing') unless return_to
|
|
358
|
+
|
|
359
|
+
trust_root = query['openid.trust_root']
|
|
360
|
+
|
|
361
|
+
unless OpenID::TrustRoot.parse(return_to)
|
|
362
|
+
raise MalformedReturnURL.new(query, return_to)
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
if trust_root and not OpenID::TrustRoot.parse(return_to)
|
|
366
|
+
raise MalformedTrustRoot.new(query, trust_root)
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
assoc_handle = query['openid.assoc_handle']
|
|
370
|
+
|
|
371
|
+
req = new(mode, identity, return_to, trust_root, assoc_handle)
|
|
372
|
+
req.query = query
|
|
373
|
+
|
|
374
|
+
unless req.trust_root_valid
|
|
375
|
+
raise UntrustedReturnURL.new(query, return_to, trust_root)
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
return req
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
# Returns +true+ or +false+ according to whether the return_to
|
|
382
|
+
# is under the supplied trust_root.
|
|
383
|
+
def trust_root_valid
|
|
384
|
+
return true unless @trust_root
|
|
385
|
+
tr = OpenID::TrustRoot.parse(@trust_root)
|
|
386
|
+
raise MalformedTrustRoot.new(nil, @trust_root) if tr.nil?
|
|
387
|
+
return tr.validate_url(@return_to)
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
# Generate a response to this checkid_* request.
|
|
391
|
+
#
|
|
392
|
+
# ==Paramters
|
|
393
|
+
#
|
|
394
|
+
# [+allow+]
|
|
395
|
+
# Boolean value stating whether or not to allow this user to "claim"
|
|
396
|
+
# supplied identity and let the consumer have the information. The
|
|
397
|
+
# value of allow should be follow the following algorithm:
|
|
398
|
+
#
|
|
399
|
+
# The identity URL provided (openid.identity) and available
|
|
400
|
+
# through the +identity_url+ method of this object is owned by
|
|
401
|
+
# the logged in user, and they have approved the consumer receive
|
|
402
|
+
# the identity assertion.
|
|
403
|
+
#
|
|
404
|
+
# [+server_url+]
|
|
405
|
+
# When an immeditate mode request does not succeed, it gets back a URL
|
|
406
|
+
# where the request may be continued in a not-so-immediate fashion.
|
|
407
|
+
# The URL returned is generated using the supplied +server_url+ here.
|
|
408
|
+
# +server_url+ should be the full URL of you openid server endpoint.
|
|
409
|
+
def answer(allow, server_url=nil)
|
|
410
|
+
if allow or @immediate
|
|
411
|
+
mode = 'id_res'
|
|
412
|
+
else
|
|
413
|
+
mode = 'cancel'
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
response = OpenIDResponse.new(self)
|
|
417
|
+
|
|
418
|
+
if allow
|
|
419
|
+
response.add_fields(nil, {
|
|
420
|
+
'mode' => mode,
|
|
421
|
+
'identity' => @identity,
|
|
422
|
+
'return_to' => @return_to
|
|
423
|
+
})
|
|
424
|
+
else
|
|
425
|
+
response.add_field(nil, 'mode', mode, false)
|
|
426
|
+
response.signed.clear
|
|
427
|
+
if @immediate
|
|
428
|
+
unless server_url
|
|
429
|
+
raise ArgumentError, "setup_url is required for allow=false in immediate mode"
|
|
430
|
+
end
|
|
431
|
+
# make a request just like this one, but immediate mode
|
|
432
|
+
setup_request = self.class.new('checkid_immediate',
|
|
433
|
+
@identity,
|
|
434
|
+
@return_to,
|
|
435
|
+
@trust_root)
|
|
436
|
+
setup_url = setup_request.encode_to_url(server_url)
|
|
437
|
+
response.add_field(nil, 'user_setup_url', setup_url, false)
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
end
|
|
441
|
+
|
|
442
|
+
return response
|
|
443
|
+
end
|
|
444
|
+
|
|
445
|
+
# Encode this request as a GET URL, returning the URL.
|
|
446
|
+
def encode_to_url(server_url)
|
|
447
|
+
q = {
|
|
448
|
+
'openid.mode' => @mode,
|
|
449
|
+
'openid.identity' => @identity,
|
|
450
|
+
'openid.return_to' => @return_to
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
q['openid.trust_root'] = @trust_root if @trust_root
|
|
454
|
+
q['openid.assoc_handle'] = @assoc_handle if @assoc_handle
|
|
455
|
+
return OpenID::Util.append_args(server_url, q)
|
|
456
|
+
end
|
|
457
|
+
|
|
458
|
+
# Create the URL to cancel this request. Useful for creating a "Cancel"
|
|
459
|
+
# button on your "approve this openid transaction" form.
|
|
460
|
+
def cancel_url
|
|
461
|
+
if @immediate
|
|
462
|
+
raise ProtocolError.new(nil, 'cancel is not an appropriate reponse to immediate mode requests')
|
|
463
|
+
end
|
|
464
|
+
return OpenID::Util.append_args(@return_to,{'openid.mode' => 'cancel'})
|
|
465
|
+
end
|
|
466
|
+
|
|
467
|
+
# The identity_url which was requested to be verified. Your server
|
|
468
|
+
# should provide a page at identity_url, and be able to assert
|
|
469
|
+
# that the logged in user does or does not "own" that URL.
|
|
470
|
+
# "Owning" an identity_url is in the details of the
|
|
471
|
+
# server account name to URL mapping.
|
|
472
|
+
def identity_url
|
|
473
|
+
@identity
|
|
474
|
+
end
|
|
475
|
+
|
|
476
|
+
end
|
|
477
|
+
|
|
478
|
+
# Object representing a response to an OpenIDRequest
|
|
479
|
+
class OpenIDResponse
|
|
480
|
+
|
|
481
|
+
attr_accessor :request, :fields, :signed
|
|
482
|
+
|
|
483
|
+
# +request+ is a subclass of OpenIDRequest that this object
|
|
484
|
+
# should respond to.
|
|
485
|
+
def initialize(request)
|
|
486
|
+
@request = request
|
|
487
|
+
@fields = {}
|
|
488
|
+
@signed = []
|
|
489
|
+
end
|
|
490
|
+
|
|
491
|
+
# Add an extra field to this response.
|
|
492
|
+
#
|
|
493
|
+
# [+namespace+]
|
|
494
|
+
# Extension namespace the field is in with no leading "openid.". For
|
|
495
|
+
# example, if you are adding a simple registration argument, you would
|
|
496
|
+
# pass 'sreg' as the namespace.
|
|
497
|
+
def add_field(namespace, key, value, signed=true)
|
|
498
|
+
if namespace and namespace != ''
|
|
499
|
+
key = namespace + '.' + key
|
|
500
|
+
end
|
|
501
|
+
@fields[key] = value
|
|
502
|
+
if signed and not @signed.member?(key)
|
|
503
|
+
@signed << key
|
|
504
|
+
end
|
|
505
|
+
end
|
|
506
|
+
|
|
507
|
+
# Same as OpenIDResponse.add_field, except that it accepts a Hash
|
|
508
|
+
# fields to be added as the +fields+ argument.
|
|
509
|
+
def add_fields(namespace, fields, signed=true)
|
|
510
|
+
fields.each {|k,v| self.add_field(namespace, k, v, signed)}
|
|
511
|
+
end
|
|
512
|
+
|
|
513
|
+
# Update the fields of this request with another OpenIDResponse, +other+.
|
|
514
|
+
def update(namespace, other)
|
|
515
|
+
if namespace and namespace != ''
|
|
516
|
+
namespaced_fields = {}
|
|
517
|
+
other.fields.each {|k,v| namespaced_fields[namespace+'.'+k] = v}
|
|
518
|
+
namespaced_signed = other.signed.collect {|k| namespace+'.'+k}
|
|
519
|
+
else
|
|
520
|
+
namespaced_fields = other.fields.dup
|
|
521
|
+
namespaced_signed = other.signed.dup
|
|
522
|
+
end
|
|
523
|
+
|
|
524
|
+
@fields.update(namespaced_fields)
|
|
525
|
+
@signed |= namespaced_signed
|
|
526
|
+
end
|
|
527
|
+
|
|
528
|
+
# Returns a boolean saying whether or not this response requires
|
|
529
|
+
# signing.
|
|
530
|
+
def needs_signing?
|
|
531
|
+
['checkid_immediate','checkid_setup'].member?(@request.mode) and \
|
|
532
|
+
@signed.length > 0
|
|
533
|
+
end
|
|
534
|
+
|
|
535
|
+
# OpenID responses can be sent back as a URL redirect or as a kvform
|
|
536
|
+
# reponse to a POST. This method returns a code describing how the
|
|
537
|
+
# response should be encoded, and return either
|
|
538
|
+
# OpenID::Server::ENCODE_URL or OpenID::Server::ENCODE_KVFORM.
|
|
539
|
+
def which_encoding?
|
|
540
|
+
if BROWSER_REQUEST_MODES.member?(@request.mode)
|
|
541
|
+
return ENCODE_URL
|
|
542
|
+
else
|
|
543
|
+
return ENCODE_KVFORM
|
|
544
|
+
end
|
|
545
|
+
end
|
|
546
|
+
|
|
547
|
+
# Encode the response to a URL, suitable to be send via 302 redirect.
|
|
548
|
+
def encode_to_url
|
|
549
|
+
fields = {}
|
|
550
|
+
@fields.each {|k,v| fields['openid.'+k] = v}
|
|
551
|
+
return OpenID::Util.append_args(@request.return_to, fields)
|
|
552
|
+
end
|
|
553
|
+
|
|
554
|
+
# Encode the response to kvform format.
|
|
555
|
+
def encode_to_kvform
|
|
556
|
+
return OpenID::Util.kvform(@fields)
|
|
557
|
+
end
|
|
558
|
+
|
|
559
|
+
end
|
|
560
|
+
|
|
561
|
+
# Object responsible for signing responses, and checking signatures.
|
|
562
|
+
class Signatory
|
|
563
|
+
@@secret_lifetime = 14 * 24 * 60 * 6
|
|
564
|
+
@@normal_key = 'http://localhost/|normal'
|
|
565
|
+
@@dumb_key = 'http://localhost/|dumb'
|
|
566
|
+
|
|
567
|
+
attr_reader :dumb_key
|
|
568
|
+
|
|
569
|
+
# Accepts an object that implements the OpenID::Store interface. See
|
|
570
|
+
# OpenID::FilesystemStore for a simple file based store.
|
|
571
|
+
def initialize(store)
|
|
572
|
+
@store = store
|
|
573
|
+
end
|
|
574
|
+
|
|
575
|
+
# Verify that the signature for some data is valid.
|
|
576
|
+
#
|
|
577
|
+
# ==Paramters
|
|
578
|
+
#
|
|
579
|
+
# [+assoc_handle+]
|
|
580
|
+
# The association handle used to get the secret out of the store, and
|
|
581
|
+
# passed in via openid.assoc_handle.
|
|
582
|
+
#
|
|
583
|
+
# [+sig+]
|
|
584
|
+
# Value of openid.sig
|
|
585
|
+
#
|
|
586
|
+
# [+signed_pairs+]
|
|
587
|
+
# Array of Array pairs of key, value signed data.
|
|
588
|
+
#
|
|
589
|
+
# [+dumb+]
|
|
590
|
+
# boolean representing whether this is a dumb mode request
|
|
591
|
+
def verify(assoc_handle, sig, signed_pairs, dumb=true)
|
|
592
|
+
assoc = self.get_association(assoc_handle, dumb)
|
|
593
|
+
unless assoc
|
|
594
|
+
OpenID::Util.log("failed to get assoc with handle #{assoc_handle} to verify sig #{sig}")
|
|
595
|
+
return false
|
|
596
|
+
end
|
|
597
|
+
|
|
598
|
+
expected_sig = OpenID::Util.to_base64(assoc.sign(signed_pairs))
|
|
599
|
+
|
|
600
|
+
if sig == expected_sig
|
|
601
|
+
return true
|
|
602
|
+
else
|
|
603
|
+
OpenID::Util.log("signture mismatch: expected #{expected_sig}, got #{sig}")
|
|
604
|
+
return false
|
|
605
|
+
end
|
|
606
|
+
end
|
|
607
|
+
|
|
608
|
+
# Sign a response, creating a signature based on everything in it's
|
|
609
|
+
# +signed+ array. Returns a new response object with sig and signed
|
|
610
|
+
# values in its field set.
|
|
611
|
+
def sign(response)
|
|
612
|
+
# get a deep copy of the response
|
|
613
|
+
signed_response = Marshal.load(Marshal.dump(response))
|
|
614
|
+
assoc_handle = response.request.assoc_handle
|
|
615
|
+
|
|
616
|
+
if assoc_handle
|
|
617
|
+
assoc = self.get_association(assoc_handle, false)
|
|
618
|
+
unless assoc
|
|
619
|
+
# no assoc for handle, fall back to dumb mode
|
|
620
|
+
signed_response.fields['invalidate_handle'] = assoc_handle
|
|
621
|
+
assoc = self.create_association(true)
|
|
622
|
+
end
|
|
623
|
+
else
|
|
624
|
+
# dumb mode
|
|
625
|
+
assoc = self.create_association(true)
|
|
626
|
+
end
|
|
627
|
+
|
|
628
|
+
signed_response.fields['assoc_handle'] = assoc.handle
|
|
629
|
+
assoc.add_signature(signed_response.signed,
|
|
630
|
+
signed_response.fields, '')
|
|
631
|
+
return signed_response
|
|
632
|
+
end
|
|
633
|
+
|
|
634
|
+
# Make a new assocation.
|
|
635
|
+
def create_association(dumb=true, assoc_type='HMAC-SHA1')
|
|
636
|
+
secret = OpenID::Util.get_random_bytes(20)
|
|
637
|
+
uniq = OpenID::Util.to_base64(OpenID::Util.get_random_bytes(4))
|
|
638
|
+
handle = "{%s}{%x}{%s}" % [assoc_type, Time.now.to_i, uniq]
|
|
639
|
+
assoc = Association.from_expires_in(@@secret_lifetime,
|
|
640
|
+
handle,
|
|
641
|
+
secret,
|
|
642
|
+
assoc_type)
|
|
643
|
+
|
|
644
|
+
key = dumb ? @@dumb_key : @@normal_key
|
|
645
|
+
@store.store_association(key, assoc)
|
|
646
|
+
return assoc
|
|
647
|
+
end
|
|
648
|
+
|
|
649
|
+
# Get an association by assoc_handle and mode
|
|
650
|
+
def get_association(assoc_handle, dumb)
|
|
651
|
+
if assoc_handle.nil?
|
|
652
|
+
raise ArgumentError, 'assoc_handle must not be nil'
|
|
653
|
+
end
|
|
654
|
+
|
|
655
|
+
key = dumb ? @@dumb_key : @@normal_key
|
|
656
|
+
|
|
657
|
+
assoc = @store.get_association(key, assoc_handle)
|
|
658
|
+
if assoc and assoc.expired?
|
|
659
|
+
@store.remove_association(key, assoc_handle)
|
|
660
|
+
assoc = nil
|
|
661
|
+
end
|
|
662
|
+
|
|
663
|
+
return assoc
|
|
664
|
+
end
|
|
665
|
+
|
|
666
|
+
# Invalidate an association by assoc_handle and mode
|
|
667
|
+
def invalidate(assoc_handle, dumb)
|
|
668
|
+
key = dumb ? @@dumb_key : @@normal_key
|
|
669
|
+
@store.remove_association(key, assoc_handle)
|
|
670
|
+
end
|
|
671
|
+
|
|
672
|
+
end
|
|
673
|
+
|
|
674
|
+
# Response to an OpenID request that a web server can understand. Proper
|
|
675
|
+
# responses can be issued to your framework by examining the +code+
|
|
676
|
+
# method and associated methods.
|
|
677
|
+
class WebResponse
|
|
678
|
+
|
|
679
|
+
attr_accessor :code, :headers, :body
|
|
680
|
+
|
|
681
|
+
def initialize
|
|
682
|
+
@code = HTTP_OK
|
|
683
|
+
@headers = {}
|
|
684
|
+
@body = ''
|
|
685
|
+
end
|
|
686
|
+
|
|
687
|
+
def set_redirect(url)
|
|
688
|
+
@code = HTTP_REDIRECT
|
|
689
|
+
@headers['location'] = url
|
|
690
|
+
end
|
|
691
|
+
|
|
692
|
+
# If this method returns true, a redirect should be issued by the
|
|
693
|
+
# webserver to the value returned by the redirect_url method.
|
|
694
|
+
#
|
|
695
|
+
# If this method returns false, the value returned by the +body+
|
|
696
|
+
# method should be returned to ther server as-is without modification.
|
|
697
|
+
def is_redirect?
|
|
698
|
+
@code == HTTP_REDIRECT
|
|
699
|
+
end
|
|
700
|
+
|
|
701
|
+
def redirect_url
|
|
702
|
+
@headers['location']
|
|
703
|
+
end
|
|
704
|
+
|
|
705
|
+
end
|
|
706
|
+
|
|
707
|
+
# Object that encodes OpenIDResponse objects into WebResponse objects.
|
|
708
|
+
class Encoder
|
|
709
|
+
|
|
710
|
+
def encode(response)
|
|
711
|
+
webresponse = WebResponse.new
|
|
712
|
+
|
|
713
|
+
case response.which_encoding?
|
|
714
|
+
when ENCODE_KVFORM
|
|
715
|
+
webresponse.code = HTTP_ERROR if response.kind_of?(Exception)
|
|
716
|
+
webresponse.body = response.encode_to_kvform
|
|
717
|
+
|
|
718
|
+
when ENCODE_URL
|
|
719
|
+
webresponse.set_redirect(response.encode_to_url)
|
|
720
|
+
|
|
721
|
+
else
|
|
722
|
+
# don't know how to encode response
|
|
723
|
+
raise EncodingError.new(response)
|
|
724
|
+
end
|
|
725
|
+
|
|
726
|
+
return webresponse
|
|
727
|
+
end
|
|
728
|
+
|
|
729
|
+
end
|
|
730
|
+
|
|
731
|
+
# Object that encodes OpenIDResponse objects into WebResponse objects,
|
|
732
|
+
# potentially adding a signature along the way.
|
|
733
|
+
class SigningEncoder < Encoder
|
|
734
|
+
|
|
735
|
+
def initialize(signatory)
|
|
736
|
+
if signatory.nil?
|
|
737
|
+
raise ArgumentError, "signatory must not be nil"
|
|
738
|
+
end
|
|
739
|
+
@signatory = signatory
|
|
740
|
+
end
|
|
741
|
+
|
|
742
|
+
def encode(response)
|
|
743
|
+
if (not response.kind_of?(Exception)) and response.needs_signing?
|
|
744
|
+
if response.fields.has_key?('sig')
|
|
745
|
+
raise AlreadySigned
|
|
746
|
+
end
|
|
747
|
+
response = @signatory.sign(response)
|
|
748
|
+
end
|
|
749
|
+
return super(response)
|
|
750
|
+
end
|
|
751
|
+
|
|
752
|
+
end
|
|
753
|
+
|
|
754
|
+
# Decode incoming web requests into an OpenIDRequest object.
|
|
755
|
+
class Decoder
|
|
756
|
+
|
|
757
|
+
def decode(query)
|
|
758
|
+
return nil if query.length == 0
|
|
759
|
+
|
|
760
|
+
mode = query['openid.mode']
|
|
761
|
+
return nil if mode.nil?
|
|
762
|
+
|
|
763
|
+
if mode.class == Array
|
|
764
|
+
raise ArgumentError, 'query hash must have one value for each key'
|
|
765
|
+
end
|
|
766
|
+
|
|
767
|
+
case mode
|
|
768
|
+
when 'checkid_setup', 'checkid_immediate'
|
|
769
|
+
return CheckIDRequest.from_query(query)
|
|
770
|
+
|
|
771
|
+
when 'check_authentication'
|
|
772
|
+
return CheckAuthRequest.from_query(query)
|
|
773
|
+
|
|
774
|
+
when 'associate'
|
|
775
|
+
return AssociateRequest.from_query(query)
|
|
776
|
+
|
|
777
|
+
else
|
|
778
|
+
raise ProtocolError.new(query, "Unknown mode #{mode}")
|
|
779
|
+
end
|
|
780
|
+
|
|
781
|
+
end
|
|
782
|
+
|
|
783
|
+
end
|
|
784
|
+
|
|
785
|
+
# Top level object that handles incoming requests for an OpenID server.
|
|
786
|
+
#
|
|
787
|
+
# Some types of requests (those which are not CheckIDRequest objects) may
|
|
788
|
+
# be handed to the handle_request method, and an appropriate response
|
|
789
|
+
# will be returned.
|
|
790
|
+
#
|
|
791
|
+
# For convenienve, decode and encode methods are exposed which should be
|
|
792
|
+
# used as the entry and exit points of the OpenID server logic. The first
|
|
793
|
+
# step when handling an OpenID server action should be to call
|
|
794
|
+
# Server.decode_request with the query arguments.
|
|
795
|
+
#
|
|
796
|
+
# This object needs an instance of OpenID::Store to store state between
|
|
797
|
+
# sessions and associations. See OpenID::FilesystemStore for a simple
|
|
798
|
+
# file based solution.
|
|
799
|
+
#
|
|
800
|
+
# ==Pseudo Code
|
|
801
|
+
#
|
|
802
|
+
# Below is some pseudo code for using this object to handle OpenID server
|
|
803
|
+
# requests. The +params+ variable represents a Hash of the incoming
|
|
804
|
+
# arguments. is_authorized and show_decide_page are methods you provide.
|
|
805
|
+
# At the end you have a WebResponse object suitable for examining and
|
|
806
|
+
# issuing a response to your web server.
|
|
807
|
+
#
|
|
808
|
+
# include OpenID
|
|
809
|
+
# store = FilesystemStore.new('/var/openid/store')
|
|
810
|
+
# server = Server::Server.new(store)
|
|
811
|
+
# request = server.decode_request(params)
|
|
812
|
+
# if request.kind_of?(CheckIDRequest)
|
|
813
|
+
# if is_authorized(request.identity, request.trust_root)
|
|
814
|
+
# response = request.answer(true)
|
|
815
|
+
# elsif request.immediate
|
|
816
|
+
# response = request.answer(false,'http://example.com/openid-server')
|
|
817
|
+
# else
|
|
818
|
+
# show_decide_page(request)
|
|
819
|
+
# return
|
|
820
|
+
# end
|
|
821
|
+
# else
|
|
822
|
+
# response = server.handle_request(request)
|
|
823
|
+
# end
|
|
824
|
+
#
|
|
825
|
+
# web_response = server.encode_response(response)
|
|
826
|
+
#
|
|
827
|
+
# For an actual working example, please see the rails_server
|
|
828
|
+
# directory inside of the examples directory. Have a look at the
|
|
829
|
+
# app/controllers/server_controller.rb and the +index+ method of the
|
|
830
|
+
# ServerController object.
|
|
831
|
+
class Server
|
|
832
|
+
|
|
833
|
+
# +store+ is a kind of OpenID::Store
|
|
834
|
+
def initialize(store)
|
|
835
|
+
@store = store
|
|
836
|
+
@signatory = Signatory.new(store)
|
|
837
|
+
@encoder = SigningEncoder.new(@signatory)
|
|
838
|
+
@decoder = Decoder.new
|
|
839
|
+
end
|
|
840
|
+
|
|
841
|
+
# Decode an incoming web request into a kind of OpenIDRequest object.
|
|
842
|
+
# +query+ should be a hash of request arguments. Rails users will want to
|
|
843
|
+
# pass in the @params instance variable of the ActionController.
|
|
844
|
+
def decode_request(query)
|
|
845
|
+
return @decoder.decode(query)
|
|
846
|
+
end
|
|
847
|
+
|
|
848
|
+
# Handle all non checkid_* OpenID requests.
|
|
849
|
+
def handle_request(request)
|
|
850
|
+
return self.send('openid_'+request.mode, request)
|
|
851
|
+
end
|
|
852
|
+
|
|
853
|
+
# Return a WebResponse object given an OpenIDResponse object
|
|
854
|
+
def encode_response(response)
|
|
855
|
+
return @encoder.encode(response)
|
|
856
|
+
end
|
|
857
|
+
|
|
858
|
+
# called by handle_request to perform check auth calls
|
|
859
|
+
def openid_check_authentication(request)
|
|
860
|
+
return request.answer(@signatory)
|
|
861
|
+
end
|
|
862
|
+
|
|
863
|
+
# called by handle_request to perform openid.mode=associate calls.
|
|
864
|
+
def openid_associate(request)
|
|
865
|
+
assoc = @signatory.create_association(false)
|
|
866
|
+
return request.answer(assoc)
|
|
867
|
+
end
|
|
868
|
+
|
|
869
|
+
end
|
|
870
|
+
|
|
871
|
+
# Raised when an OpenID request encounters some kind of protocol error.
|
|
872
|
+
class ProtocolError < Exception
|
|
873
|
+
|
|
874
|
+
attr_reader :query
|
|
875
|
+
|
|
876
|
+
def initialize(query, text=nil)
|
|
877
|
+
super(text)
|
|
878
|
+
@query = query
|
|
879
|
+
end
|
|
880
|
+
|
|
881
|
+
def has_return_to?
|
|
882
|
+
@query.has_key?('openid.return_to')
|
|
883
|
+
end
|
|
884
|
+
|
|
885
|
+
def encode_to_url
|
|
886
|
+
return_to = @query['openid.return_to']
|
|
887
|
+
unless return_to
|
|
888
|
+
raise ArgumentError, 'No return_to in query'
|
|
889
|
+
end
|
|
890
|
+
|
|
891
|
+
args = {
|
|
892
|
+
'openid.mode' => 'error',
|
|
893
|
+
'openid.error' => self.to_s
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
return OpenID::Util.append_args(return_to, args)
|
|
897
|
+
end
|
|
898
|
+
|
|
899
|
+
def encode_to_kvform
|
|
900
|
+
args = {
|
|
901
|
+
'mode' => 'error',
|
|
902
|
+
'error' => self.to_s
|
|
903
|
+
}
|
|
904
|
+
return OpenID::Util.kvform(args)
|
|
905
|
+
end
|
|
906
|
+
|
|
907
|
+
def which_encoding?
|
|
908
|
+
if self.has_return_to?
|
|
909
|
+
return ENCODE_URL
|
|
910
|
+
end
|
|
911
|
+
|
|
912
|
+
mode = @query['openid.mode']
|
|
913
|
+
if mode and not BROWSER_REQUEST_MODES.member?(mode)
|
|
914
|
+
return ENCODE_KVFORM
|
|
915
|
+
end
|
|
916
|
+
|
|
917
|
+
return nil
|
|
918
|
+
end
|
|
919
|
+
|
|
920
|
+
end
|
|
921
|
+
|
|
922
|
+
class EncodingError < Exception; end
|
|
923
|
+
class AlreadySigned < EncodingError; end
|
|
924
|
+
|
|
925
|
+
class MalformedReturnURL < ProtocolError
|
|
926
|
+
|
|
927
|
+
attr_reader :return_to
|
|
928
|
+
|
|
929
|
+
def initialize(query, return_to)
|
|
930
|
+
super(query)
|
|
931
|
+
@return_to = return_to
|
|
932
|
+
end
|
|
933
|
+
|
|
934
|
+
end
|
|
935
|
+
|
|
936
|
+
class MalformedTrustRoot < ProtocolError; end
|
|
937
|
+
|
|
938
|
+
class UntrustedReturnURL < ProtocolError
|
|
939
|
+
|
|
940
|
+
attr_reader :return_to, :trust_root
|
|
941
|
+
|
|
942
|
+
def initialize(query, return_to, trust_root)
|
|
943
|
+
super(query)
|
|
944
|
+
@return_to = return_to
|
|
945
|
+
@trust_root = trust_root
|
|
946
|
+
end
|
|
947
|
+
end
|
|
948
|
+
|
|
949
|
+
end
|
|
950
|
+
|
|
951
|
+
end
|