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.
- checksums.yaml +7 -0
- checksums.yaml.gz.sig +0 -0
- data/CHANGELOG.md +136 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/CONTRIBUTING.md +54 -0
- data/LICENSE.txt +210 -0
- data/README.md +81 -0
- data/SECURITY.md +15 -0
- data/lib/hmac/hmac.rb +110 -0
- data/lib/hmac/sha1.rb +11 -0
- data/lib/hmac/sha2.rb +25 -0
- data/lib/openid/association.rb +246 -0
- data/lib/openid/consumer/associationmanager.rb +354 -0
- data/lib/openid/consumer/checkid_request.rb +179 -0
- data/lib/openid/consumer/discovery.rb +516 -0
- data/lib/openid/consumer/discovery_manager.rb +144 -0
- data/lib/openid/consumer/html_parse.rb +142 -0
- data/lib/openid/consumer/idres.rb +513 -0
- data/lib/openid/consumer/responses.rb +147 -0
- data/lib/openid/consumer/session.rb +36 -0
- data/lib/openid/consumer.rb +406 -0
- data/lib/openid/cryptutil.rb +112 -0
- data/lib/openid/dh.rb +84 -0
- data/lib/openid/extension.rb +38 -0
- data/lib/openid/extensions/ax.rb +552 -0
- data/lib/openid/extensions/oauth.rb +88 -0
- data/lib/openid/extensions/pape.rb +170 -0
- data/lib/openid/extensions/sreg.rb +268 -0
- data/lib/openid/extensions/ui.rb +49 -0
- data/lib/openid/fetchers.rb +277 -0
- data/lib/openid/kvform.rb +113 -0
- data/lib/openid/kvpost.rb +62 -0
- data/lib/openid/message.rb +555 -0
- data/lib/openid/protocolerror.rb +7 -0
- data/lib/openid/server.rb +1571 -0
- data/lib/openid/store/filesystem.rb +260 -0
- data/lib/openid/store/interface.rb +73 -0
- data/lib/openid/store/memcache.rb +109 -0
- data/lib/openid/store/memory.rb +79 -0
- data/lib/openid/store/nonce.rb +72 -0
- data/lib/openid/trustroot.rb +597 -0
- data/lib/openid/urinorm.rb +72 -0
- data/lib/openid/util.rb +119 -0
- data/lib/openid/version.rb +5 -0
- data/lib/openid/yadis/accept.rb +141 -0
- data/lib/openid/yadis/constants.rb +16 -0
- data/lib/openid/yadis/discovery.rb +151 -0
- data/lib/openid/yadis/filters.rb +192 -0
- data/lib/openid/yadis/htmltokenizer.rb +290 -0
- data/lib/openid/yadis/parsehtml.rb +50 -0
- data/lib/openid/yadis/services.rb +44 -0
- data/lib/openid/yadis/xrds.rb +160 -0
- data/lib/openid/yadis/xri.rb +86 -0
- data/lib/openid/yadis/xrires.rb +87 -0
- data/lib/openid.rb +27 -0
- data/lib/ruby-openid.rb +1 -0
- data.tar.gz.sig +0 -0
- metadata +331 -0
- 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
|