ruby-openid2 3.0.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.
Files changed (59) hide show
  1. checksums.yaml +7 -0
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +136 -0
  4. data/CODE_OF_CONDUCT.md +84 -0
  5. data/CONTRIBUTING.md +54 -0
  6. data/LICENSE.txt +210 -0
  7. data/README.md +81 -0
  8. data/SECURITY.md +15 -0
  9. data/lib/hmac/hmac.rb +110 -0
  10. data/lib/hmac/sha1.rb +11 -0
  11. data/lib/hmac/sha2.rb +25 -0
  12. data/lib/openid/association.rb +246 -0
  13. data/lib/openid/consumer/associationmanager.rb +354 -0
  14. data/lib/openid/consumer/checkid_request.rb +179 -0
  15. data/lib/openid/consumer/discovery.rb +516 -0
  16. data/lib/openid/consumer/discovery_manager.rb +144 -0
  17. data/lib/openid/consumer/html_parse.rb +142 -0
  18. data/lib/openid/consumer/idres.rb +513 -0
  19. data/lib/openid/consumer/responses.rb +147 -0
  20. data/lib/openid/consumer/session.rb +36 -0
  21. data/lib/openid/consumer.rb +406 -0
  22. data/lib/openid/cryptutil.rb +112 -0
  23. data/lib/openid/dh.rb +84 -0
  24. data/lib/openid/extension.rb +38 -0
  25. data/lib/openid/extensions/ax.rb +552 -0
  26. data/lib/openid/extensions/oauth.rb +88 -0
  27. data/lib/openid/extensions/pape.rb +170 -0
  28. data/lib/openid/extensions/sreg.rb +268 -0
  29. data/lib/openid/extensions/ui.rb +49 -0
  30. data/lib/openid/fetchers.rb +277 -0
  31. data/lib/openid/kvform.rb +113 -0
  32. data/lib/openid/kvpost.rb +62 -0
  33. data/lib/openid/message.rb +555 -0
  34. data/lib/openid/protocolerror.rb +7 -0
  35. data/lib/openid/server.rb +1571 -0
  36. data/lib/openid/store/filesystem.rb +260 -0
  37. data/lib/openid/store/interface.rb +73 -0
  38. data/lib/openid/store/memcache.rb +109 -0
  39. data/lib/openid/store/memory.rb +79 -0
  40. data/lib/openid/store/nonce.rb +72 -0
  41. data/lib/openid/trustroot.rb +597 -0
  42. data/lib/openid/urinorm.rb +72 -0
  43. data/lib/openid/util.rb +119 -0
  44. data/lib/openid/version.rb +5 -0
  45. data/lib/openid/yadis/accept.rb +141 -0
  46. data/lib/openid/yadis/constants.rb +16 -0
  47. data/lib/openid/yadis/discovery.rb +151 -0
  48. data/lib/openid/yadis/filters.rb +192 -0
  49. data/lib/openid/yadis/htmltokenizer.rb +290 -0
  50. data/lib/openid/yadis/parsehtml.rb +50 -0
  51. data/lib/openid/yadis/services.rb +44 -0
  52. data/lib/openid/yadis/xrds.rb +160 -0
  53. data/lib/openid/yadis/xri.rb +86 -0
  54. data/lib/openid/yadis/xrires.rb +87 -0
  55. data/lib/openid.rb +27 -0
  56. data/lib/ruby-openid.rb +1 -0
  57. data.tar.gz.sig +0 -0
  58. metadata +331 -0
  59. metadata.gz.sig +0 -0
@@ -0,0 +1,277 @@
1
+ # External dependencies
2
+ require "net/http"
3
+
4
+ # This library
5
+ require_relative "util"
6
+
7
+ begin
8
+ require "net/https"
9
+ rescue LoadError
10
+ OpenID::Util.log("WARNING: no SSL support found. Will not be able " +
11
+ "to fetch HTTPS URLs!")
12
+ require "net/http"
13
+ end
14
+
15
+ MAX_RESPONSE_KB = 10_485_760 # 10 MB (can be smaller, I guess)
16
+
17
+ module Net
18
+ class HTTP
19
+ def post_connection_check(hostname)
20
+ check_common_name = true
21
+ cert = @socket.io.peer_cert
22
+ cert.extensions.each do |ext|
23
+ next if ext.oid != "subjectAltName"
24
+
25
+ ext.value.split(/,\s+/).each do |general_name|
26
+ if /\ADNS:(.*)/ =~ general_name
27
+ check_common_name = false
28
+ reg = Regexp.escape(::Regexp.last_match(1)).gsub("\\*", "[^.]+")
29
+ return true if /\A#{reg}\z/i.match?(hostname)
30
+ elsif /\AIP Address:(.*)/ =~ general_name
31
+ check_common_name = false
32
+ return true if ::Regexp.last_match(1) == hostname
33
+ end
34
+ end
35
+ end
36
+ if check_common_name
37
+ cert.subject.to_a.each do |oid, value|
38
+ if oid == "CN"
39
+ reg = Regexp.escape(value).gsub("\\*", "[^.]+")
40
+ return true if /\A#{reg}\z/i.match?(hostname)
41
+ end
42
+ end
43
+ end
44
+ raise OpenSSL::SSL::SSLError, "hostname does not match"
45
+ end
46
+ end
47
+ end
48
+
49
+ module OpenID
50
+ # Our HTTPResponse class extends Net::HTTPResponse with an additional
51
+ # method, final_url.
52
+ class HTTPResponse
53
+ attr_accessor :final_url, :_response
54
+
55
+ class << self
56
+ def _from_net_response(response, final_url, _headers = nil)
57
+ instance = new
58
+ instance._response = response
59
+ instance.final_url = final_url
60
+ instance
61
+ end
62
+ end
63
+
64
+ def method_missing(method, *args)
65
+ @_response.send(method, *args)
66
+ end
67
+
68
+ def respond_to_missing?(method_name, include_private = false)
69
+ super
70
+ end
71
+
72
+ def body=(s)
73
+ @_response.instance_variable_set(:@body, s)
74
+ # XXX Hack to work around ruby's HTTP library behavior. @body
75
+ # is only returned if it has been read from the response
76
+ # object's socket, but since we're not using a socket in this
77
+ # case, we need to set the @read flag to true to avoid a bug in
78
+ # Net::HTTPResponse.stream_check when @socket is nil.
79
+ @_response.instance_variable_set(:@read, true)
80
+ end
81
+ end
82
+
83
+ class FetchingError < OpenIDError
84
+ end
85
+
86
+ class HTTPRedirectLimitReached < FetchingError
87
+ end
88
+
89
+ class SSLFetchingError < FetchingError
90
+ end
91
+
92
+ @fetcher = nil
93
+
94
+ def self.fetch(url, body = nil, headers = nil,
95
+ redirect_limit = StandardFetcher::REDIRECT_LIMIT)
96
+ fetcher.fetch(url, body, headers, redirect_limit)
97
+ end
98
+
99
+ def self.fetcher
100
+ @fetcher = StandardFetcher.new if @fetcher.nil?
101
+
102
+ @fetcher
103
+ end
104
+
105
+ def self.fetcher=(fetcher)
106
+ @fetcher = fetcher
107
+ end
108
+
109
+ # Set the default fetcher to use the HTTP proxy defined in the environment
110
+ # variable 'http_proxy'.
111
+ def self.fetcher_use_env_http_proxy
112
+ proxy_string = ENV["http_proxy"]
113
+ return unless proxy_string
114
+
115
+ proxy_uri = URI.parse(proxy_string)
116
+ @fetcher = StandardFetcher.new(
117
+ proxy_uri.host,
118
+ proxy_uri.port,
119
+ proxy_uri.user,
120
+ proxy_uri.password,
121
+ )
122
+ end
123
+
124
+ class StandardFetcher
125
+ USER_AGENT = "ruby-openid/#{OpenID::Version::VERSION} (#{RUBY_PLATFORM})"
126
+
127
+ REDIRECT_LIMIT = 5
128
+ TIMEOUT = ENV["RUBY_OPENID_FETCHER_TIMEOUT"] || 60
129
+
130
+ attr_accessor :ca_file, :timeout, :ssl_verify_peer
131
+
132
+ # I can fetch through a HTTP proxy; arguments are as for Net::HTTP::Proxy.
133
+ def initialize(proxy_addr = nil, proxy_port = nil,
134
+ proxy_user = nil, proxy_pass = nil)
135
+ @ca_file = nil
136
+ @proxy = Net::HTTP::Proxy(proxy_addr, proxy_port, proxy_user, proxy_pass)
137
+ @timeout = TIMEOUT
138
+ @ssl_verify_peer = nil
139
+ end
140
+
141
+ def supports_ssl?(conn)
142
+ conn.respond_to?(:use_ssl=)
143
+ end
144
+
145
+ def make_http(uri)
146
+ http = @proxy.new(uri.host, uri.port)
147
+ http.read_timeout = @timeout
148
+ http.open_timeout = @timeout
149
+ http
150
+ end
151
+
152
+ def set_verified(conn, verify)
153
+ conn.verify_mode = if verify
154
+ OpenSSL::SSL::VERIFY_PEER
155
+ else
156
+ OpenSSL::SSL::VERIFY_NONE
157
+ end
158
+ end
159
+
160
+ def make_connection(uri)
161
+ conn = make_http(uri)
162
+
163
+ unless conn.is_a?(Net::HTTP)
164
+ raise format(
165
+ "Expected Net::HTTP object from make_http; got %s",
166
+ conn.class,
167
+ ).to_s
168
+ end
169
+
170
+ if uri.scheme == "https"
171
+ raise "SSL support not found; cannot fetch #{uri}" unless supports_ssl?(conn)
172
+
173
+ conn.use_ssl = true
174
+
175
+ if @ca_file
176
+ set_verified(conn, true)
177
+ conn.ca_file = @ca_file
178
+ elsif @ssl_verify_peer
179
+ set_verified(conn, true)
180
+ else
181
+ Util.log("WARNING: making https request to #{uri} without verifying " +
182
+ "server certificate; no CA path was specified.")
183
+ set_verified(conn, false)
184
+ end
185
+
186
+ end
187
+
188
+ conn
189
+ end
190
+
191
+ def fetch(url, body = nil, headers = nil, redirect_limit = REDIRECT_LIMIT)
192
+ unparsed_url = url.dup
193
+ url = URI.parse(url)
194
+ raise FetchingError, "Invalid URL: #{unparsed_url}" if url.nil?
195
+
196
+ headers ||= {}
197
+ headers["User-agent"] ||= USER_AGENT
198
+
199
+ begin
200
+ conn = make_connection(url)
201
+ response = nil
202
+
203
+ whole_body = ""
204
+ body_size_limitter = lambda do |r|
205
+ r.read_body do |partial| # read body now
206
+ whole_body << partial
207
+ raise FetchingError.new("Response Too Large") if whole_body.length > MAX_RESPONSE_KB
208
+ end
209
+ whole_body
210
+ end
211
+ response = conn.start do
212
+ # Check the certificate against the URL's hostname
213
+ conn.post_connection_check(url.host) if supports_ssl?(conn) and conn.use_ssl?
214
+
215
+ if body.nil?
216
+ conn.request_get(url.request_uri, headers, &body_size_limitter)
217
+ else
218
+ headers["Content-type"] ||= "application/x-www-form-urlencoded"
219
+ conn.request_post(url.request_uri, body, headers, &body_size_limitter)
220
+ end
221
+ end
222
+ rescue Timeout::Error => e
223
+ raise FetchingError, "Error fetching #{url}: #{e}"
224
+ rescue RuntimeError => e
225
+ raise e
226
+ rescue OpenSSL::SSL::SSLError => e
227
+ raise SSLFetchingError, "Error connecting to SSL URL #{url}: #{e}"
228
+ rescue FetchingError => e
229
+ raise e
230
+ rescue Exception => e
231
+ raise FetchingError, "Error fetching #{url}: #{e}"
232
+ end
233
+
234
+ case response
235
+ when Net::HTTPRedirection
236
+ if redirect_limit <= 0
237
+ raise HTTPRedirectLimitReached.new(
238
+ "Too many redirects, not fetching #{response["location"]}",
239
+ )
240
+ end
241
+ begin
242
+ fetch(response["location"], body, headers, redirect_limit - 1)
243
+ rescue HTTPRedirectLimitReached => e
244
+ raise e
245
+ rescue FetchingError => e
246
+ raise FetchingError, "Error encountered in redirect from #{url}: #{e}"
247
+ end
248
+ else
249
+ response = HTTPResponse._from_net_response(response, unparsed_url)
250
+ response.body = whole_body
251
+ setup_encoding(response)
252
+ response
253
+ end
254
+ end
255
+
256
+ private
257
+
258
+ def setup_encoding(response)
259
+ return unless defined?(Encoding.default_external)
260
+ return unless charset = response.type_params["charset"]
261
+
262
+ begin
263
+ encoding = Encoding.find(charset)
264
+ rescue ArgumentError
265
+ # NOOP
266
+ end
267
+ encoding ||= Encoding.default_external
268
+
269
+ body = response.body
270
+ if body.respond_to?(:force_encoding)
271
+ body.force_encoding(encoding)
272
+ else
273
+ body.set_encoding(encoding)
274
+ end
275
+ end
276
+ end
277
+ end
@@ -0,0 +1,113 @@
1
+ module OpenID
2
+ class KVFormError < Exception
3
+ end
4
+
5
+ module Util
6
+ def self.seq_to_kv(seq, strict = false)
7
+ # Represent a sequence of pairs of strings as newline-terminated
8
+ # key:value pairs. The pairs are generated in the order given.
9
+ #
10
+ # @param seq: The pairs
11
+ #
12
+ # returns a string representation of the sequence
13
+ err = lambda { |msg|
14
+ msg = "seq_to_kv warning: #{msg}: #{seq.inspect}"
15
+ raise KVFormError, msg if strict
16
+
17
+ Util.log(msg)
18
+ }
19
+
20
+ lines = []
21
+ seq.each do |k, v|
22
+ unless k.is_a?(String)
23
+ err.call("Converting key to string: #{k.inspect}")
24
+ k = k.to_s
25
+ end
26
+
27
+ raise KVFormError, "Invalid input for seq_to_kv: key contains newline: #{k.inspect}" unless k.index("\n").nil?
28
+
29
+ raise KVFormError, "Invalid input for seq_to_kv: key contains colon: #{k.inspect}" unless k.index(":").nil?
30
+
31
+ err.call("Key has whitespace at beginning or end: #{k.inspect}") if k.strip != k
32
+
33
+ unless v.is_a?(String)
34
+ err.call("Converting value to string: #{v.inspect}")
35
+ v = v.to_s
36
+ end
37
+
38
+ raise KVFormError, "Invalid input for seq_to_kv: value contains newline: #{v.inspect}" unless v.index("\n").nil?
39
+
40
+ err.call("Value has whitespace at beginning or end: #{v.inspect}") if v.strip != v
41
+
42
+ lines << k + ":" + v + "\n"
43
+ end
44
+
45
+ lines.join("")
46
+ end
47
+
48
+ def self.kv_to_seq(data, strict = false)
49
+ # After one parse, seq_to_kv and kv_to_seq are inverses, with no
50
+ # warnings:
51
+ #
52
+ # seq = kv_to_seq(s)
53
+ # seq_to_kv(kv_to_seq(seq)) == seq
54
+ err = lambda { |msg|
55
+ msg = "kv_to_seq warning: #{msg}: #{data.inspect}"
56
+ raise KVFormError, msg if strict
57
+
58
+ Util.log(msg)
59
+ }
60
+
61
+ lines = data.split("\n")
62
+ return [] if data.empty?
63
+
64
+ if data[-1].chr != "\n"
65
+ err.call("Does not end in a newline")
66
+ # We don't expect the last element of lines to be an empty
67
+ # string because split() doesn't behave that way.
68
+ end
69
+
70
+ pairs = []
71
+ line_num = 0
72
+ lines.each do |line|
73
+ line_num += 1
74
+
75
+ # Ignore blank lines
76
+ next if line.strip == ""
77
+
78
+ pair = line.split(":", 2)
79
+ if pair.length == 2
80
+ k, v = pair
81
+ k_s = k.strip
82
+ if k_s != k
83
+ msg = "In line #{line_num}, ignoring leading or trailing whitespace in key #{k.inspect}"
84
+ err.call(msg)
85
+ end
86
+
87
+ err.call("In line #{line_num}, got empty key") if k_s.empty?
88
+
89
+ v_s = v.strip
90
+ if v_s != v
91
+ msg = "In line #{line_num}, ignoring leading or trailing whitespace in value #{v.inspect}"
92
+ err.call(msg)
93
+ end
94
+
95
+ pairs << [k_s, v_s]
96
+ else
97
+ err.call("Line #{line_num} does not contain a colon")
98
+ end
99
+ end
100
+
101
+ pairs
102
+ end
103
+
104
+ def self.dict_to_kv(d)
105
+ seq_to_kv(d.entries.sort)
106
+ end
107
+
108
+ def self.kv_to_dict(s)
109
+ seq = kv_to_seq(s)
110
+ Hash[*seq.flatten]
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,62 @@
1
+ require_relative "message"
2
+ require_relative "fetchers"
3
+
4
+ module OpenID
5
+ # Exception that is raised when the server returns a 400 response
6
+ # code to a direct request.
7
+ class ServerError < OpenIDError
8
+ attr_reader :error_text, :error_code, :message
9
+
10
+ def initialize(error_text, error_code, message)
11
+ super(error_text)
12
+ @error_text = error_text
13
+ @error_code = error_code
14
+ @message = message
15
+ end
16
+
17
+ def self.from_message(msg)
18
+ error_text = msg.get_arg(
19
+ OPENID_NS,
20
+ "error",
21
+ "<no error message supplied>",
22
+ )
23
+ error_code = msg.get_arg(OPENID_NS, "error_code")
24
+ new(error_text, error_code, msg)
25
+ end
26
+ end
27
+
28
+ class KVPostNetworkError < OpenIDError
29
+ end
30
+
31
+ class HTTPStatusError < OpenIDError
32
+ end
33
+
34
+ class Message
35
+ def self.from_http_response(response, server_url)
36
+ msg = from_kvform(response.body)
37
+ case response.code.to_i
38
+ when 200
39
+ msg
40
+ when 206
41
+ msg
42
+ when 400
43
+ raise ServerError.from_message(msg)
44
+ else
45
+ error_message = "bad status code from server #{server_url}: " \
46
+ "#{response.code}"
47
+ raise HTTPStatusError.new(error_message)
48
+ end
49
+ end
50
+ end
51
+
52
+ # Send the message to the server via HTTP POST and receive and parse
53
+ # a response in KV Form
54
+ def self.make_kv_post(request_message, server_url)
55
+ begin
56
+ http_response = fetch(server_url, request_message.to_url_encoded)
57
+ rescue Exception
58
+ raise KVPostNetworkError.new("Unable to contact OpenID server: #{$!}")
59
+ end
60
+ Message.from_http_response(http_response, server_url)
61
+ end
62
+ end