ruby-openid2 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
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