trustroot.rb
349 lines
| 10.6 KiB
| text/x-ruby
|
RubyLexer
|
r2376 | require 'uri' | ||
require 'openid/urinorm' | ||||
module OpenID | ||||
class RealmVerificationRedirected < Exception | ||||
# Attempting to verify this realm resulted in a redirect. | ||||
def initialize(relying_party_url, rp_url_after_redirects) | ||||
@relying_party_url = relying_party_url | ||||
@rp_url_after_redirects = rp_url_after_redirects | ||||
end | ||||
def to_s | ||||
return "Attempting to verify #{@relying_party_url} resulted in " + | ||||
"redirect to #{@rp_url_after_redirects}" | ||||
end | ||||
end | ||||
module TrustRoot | ||||
TOP_LEVEL_DOMAINS = %w' | ||||
ac ad ae aero af ag ai al am an ao aq ar arpa as asia at | ||||
au aw ax az ba bb bd be bf bg bh bi biz bj bm bn bo br bs bt | ||||
bv bw by bz ca cat cc cd cf cg ch ci ck cl cm cn co com coop | ||||
cr cu cv cx cy cz de dj dk dm do dz ec edu ee eg er es et eu | ||||
fi fj fk fm fo fr ga gb gd ge gf gg gh gi gl gm gn gov gp gq | ||||
gr gs gt gu gw gy hk hm hn hr ht hu id ie il im in info int | ||||
io iq ir is it je jm jo jobs jp ke kg kh ki km kn kp kr kw | ||||
ky kz la lb lc li lk lr ls lt lu lv ly ma mc md me mg mh mil | ||||
mk ml mm mn mo mobi mp mq mr ms mt mu museum mv mw mx my mz | ||||
na name nc ne net nf ng ni nl no np nr nu nz om org pa pe pf | ||||
pg ph pk pl pm pn pr pro ps pt pw py qa re ro rs ru rw sa sb | ||||
sc sd se sg sh si sj sk sl sm sn so sr st su sv sy sz tc td | ||||
tel tf tg th tj tk tl tm tn to tp tr travel tt tv tw tz ua | ||||
ug uk us uy uz va vc ve vg vi vn vu wf ws xn--0zwm56d | ||||
xn--11b5bs3a9aj6g xn--80akhbyknj4f xn--9t4b11yi5a | ||||
xn--deba0ad xn--g6w251d xn--hgbk6aj7f53bba | ||||
xn--hlcj6aya9esc7a xn--jxalpdlp xn--kgbechtv xn--zckzah ye | ||||
yt yu za zm zw' | ||||
ALLOWED_PROTOCOLS = ['http', 'https'] | ||||
# The URI for relying party discovery, used in realm verification. | ||||
# | ||||
# XXX: This should probably live somewhere else (like in | ||||
# OpenID or OpenID::Yadis somewhere) | ||||
RP_RETURN_TO_URL_TYPE = 'http://specs.openid.net/auth/2.0/return_to' | ||||
# If the endpoint is a relying party OpenID return_to endpoint, | ||||
# return the endpoint URL. Otherwise, return None. | ||||
# | ||||
# This function is intended to be used as a filter for the Yadis | ||||
# filtering interface. | ||||
# | ||||
# endpoint: An XRDS BasicServiceEndpoint, as returned by | ||||
# performing Yadis dicovery. | ||||
# | ||||
# returns the endpoint URL or None if the endpoint is not a | ||||
# relying party endpoint. | ||||
def TrustRoot._extract_return_url(endpoint) | ||||
if endpoint.matchTypes([RP_RETURN_TO_URL_TYPE]) | ||||
return endpoint.uri | ||||
else | ||||
return nil | ||||
end | ||||
end | ||||
# Is the return_to URL under one of the supplied allowed | ||||
# return_to URLs? | ||||
def TrustRoot.return_to_matches(allowed_return_to_urls, return_to) | ||||
allowed_return_to_urls.each { |allowed_return_to| | ||||
# A return_to pattern works the same as a realm, except that | ||||
# it's not allowed to use a wildcard. We'll model this by | ||||
# parsing it as a realm, and not trying to match it if it has | ||||
# a wildcard. | ||||
return_realm = TrustRoot.parse(allowed_return_to) | ||||
if (# Parses as a trust root | ||||
!return_realm.nil? and | ||||
# Does not have a wildcard | ||||
!return_realm.wildcard and | ||||
# Matches the return_to that we passed in with it | ||||
return_realm.validate_url(return_to) | ||||
) | ||||
return true | ||||
end | ||||
} | ||||
# No URL in the list matched | ||||
return false | ||||
end | ||||
# Given a relying party discovery URL return a list of return_to | ||||
# URLs. | ||||
def TrustRoot.get_allowed_return_urls(relying_party_url) | ||||
rp_url_after_redirects, return_to_urls = services.get_service_endpoints( | ||||
relying_party_url, _extract_return_url) | ||||
if rp_url_after_redirects != relying_party_url | ||||
# Verification caused a redirect | ||||
raise RealmVerificationRedirected.new( | ||||
relying_party_url, rp_url_after_redirects) | ||||
end | ||||
return return_to_urls | ||||
end | ||||
# Verify that a return_to URL is valid for the given realm. | ||||
# | ||||
# This function builds a discovery URL, performs Yadis discovery | ||||
# on it, makes sure that the URL does not redirect, parses out | ||||
# the return_to URLs, and finally checks to see if the current | ||||
# return_to URL matches the return_to. | ||||
# | ||||
# raises DiscoveryFailure when Yadis discovery fails returns | ||||
# true if the return_to URL is valid for the realm | ||||
def TrustRoot.verify_return_to(realm_str, return_to, _vrfy=nil) | ||||
# _vrfy parameter is there to make testing easier | ||||
if _vrfy.nil? | ||||
_vrfy = self.method('get_allowed_return_urls') | ||||
end | ||||
if !(_vrfy.is_a?(Proc) or _vrfy.is_a?(Method)) | ||||
raise ArgumentError, "_vrfy must be a Proc or Method" | ||||
end | ||||
realm = TrustRoot.parse(realm_str) | ||||
if realm.nil? | ||||
# The realm does not parse as a URL pattern | ||||
return false | ||||
end | ||||
begin | ||||
allowable_urls = _vrfy.call(realm.build_discovery_url()) | ||||
rescue RealmVerificationRedirected => err | ||||
Util.log(err.to_s) | ||||
return false | ||||
end | ||||
if return_to_matches(allowable_urls, return_to) | ||||
return true | ||||
else | ||||
Util.log("Failed to validate return_to #{return_to} for " + | ||||
"realm #{realm_str}, was not in #{allowable_urls}") | ||||
return false | ||||
end | ||||
end | ||||
class TrustRoot | ||||
attr_reader :unparsed, :proto, :wildcard, :host, :port, :path | ||||
@@empty_re = Regexp.new('^http[s]*:\/\/\*\/$') | ||||
def TrustRoot._build_path(path, query=nil, frag=nil) | ||||
s = path.dup | ||||
frag = nil if frag == '' | ||||
query = nil if query == '' | ||||
if query | ||||
s << "?" << query | ||||
end | ||||
if frag | ||||
s << "#" << frag | ||||
end | ||||
return s | ||||
end | ||||
def TrustRoot._parse_url(url) | ||||
begin | ||||
url = URINorm.urinorm(url) | ||||
rescue URI::InvalidURIError => err | ||||
nil | ||||
end | ||||
begin | ||||
parsed = URI::parse(url) | ||||
rescue URI::InvalidURIError | ||||
return nil | ||||
end | ||||
path = TrustRoot._build_path(parsed.path, | ||||
parsed.query, | ||||
parsed.fragment) | ||||
return [parsed.scheme || '', parsed.host || '', | ||||
parsed.port || '', path || ''] | ||||
end | ||||
def TrustRoot.parse(trust_root) | ||||
trust_root = trust_root.dup | ||||
unparsed = trust_root.dup | ||||
# look for wildcard | ||||
wildcard = (not trust_root.index('://*.').nil?) | ||||
trust_root.sub!('*.', '') if wildcard | ||||
# handle http://*/ case | ||||
if not wildcard and @@empty_re.match(trust_root) | ||||
proto = trust_root.split(':')[0] | ||||
port = proto == 'http' ? 80 : 443 | ||||
return new(unparsed, proto, true, '', port, '/') | ||||
end | ||||
parts = TrustRoot._parse_url(trust_root) | ||||
return nil if parts.nil? | ||||
proto, host, port, path = parts | ||||
# check for URI fragment | ||||
if path and !path.index('#').nil? | ||||
return nil | ||||
end | ||||
return nil unless ['http', 'https'].member?(proto) | ||||
return new(unparsed, proto, wildcard, host, port, path) | ||||
end | ||||
def TrustRoot.check_sanity(trust_root_string) | ||||
trust_root = TrustRoot.parse(trust_root_string) | ||||
if trust_root.nil? | ||||
return false | ||||
else | ||||
return trust_root.sane? | ||||
end | ||||
end | ||||
# quick func for validating a url against a trust root. See the | ||||
# TrustRoot class if you need more control. | ||||
def self.check_url(trust_root, url) | ||||
tr = self.parse(trust_root) | ||||
return (!tr.nil? and tr.validate_url(url)) | ||||
end | ||||
# Return a discovery URL for this realm. | ||||
# | ||||
# This function does not check to make sure that the realm is | ||||
# valid. Its behaviour on invalid inputs is undefined. | ||||
# | ||||
# return_to:: The relying party return URL of the OpenID | ||||
# authentication request | ||||
# | ||||
# Returns the URL upon which relying party discovery should be | ||||
# run in order to verify the return_to URL | ||||
def build_discovery_url | ||||
if self.wildcard | ||||
# Use "www." in place of the star | ||||
www_domain = 'www.' + @host | ||||
port = (!@port.nil? and ![80, 443].member?(@port)) ? (":" + @port.to_s) : '' | ||||
return "#{@proto}://#{www_domain}#{port}#{@path}" | ||||
else | ||||
return @unparsed | ||||
end | ||||
end | ||||
def initialize(unparsed, proto, wildcard, host, port, path) | ||||
@unparsed = unparsed | ||||
@proto = proto | ||||
@wildcard = wildcard | ||||
@host = host | ||||
@port = port | ||||
@path = path | ||||
end | ||||
def sane? | ||||
return true if @host == 'localhost' | ||||
host_parts = @host.split('.') | ||||
# a note: ruby string split does not put an empty string at | ||||
# the end of the list if the split element is last. for | ||||
# example, 'foo.com.'.split('.') => ['foo','com']. Mentioned | ||||
# because the python code differs here. | ||||
return false if host_parts.length == 0 | ||||
# no adjacent dots | ||||
return false if host_parts.member?('') | ||||
# last part must be a tld | ||||
tld = host_parts[-1] | ||||
return false unless TOP_LEVEL_DOMAINS.member?(tld) | ||||
return false if host_parts.length == 1 | ||||
if @wildcard | ||||
if tld.length == 2 and host_parts[-2].length <= 3 | ||||
# It's a 2-letter tld with a short second to last segment | ||||
# so there needs to be more than two segments specified | ||||
# (e.g. *.co.uk is insane) | ||||
return host_parts.length > 2 | ||||
end | ||||
end | ||||
return true | ||||
end | ||||
def validate_url(url) | ||||
parts = TrustRoot._parse_url(url) | ||||
return false if parts.nil? | ||||
proto, host, port, path = parts | ||||
return false unless proto == @proto | ||||
return false unless port == @port | ||||
return false unless host.index('*').nil? | ||||
if !@wildcard | ||||
if host != @host | ||||
return false | ||||
end | ||||
elsif ((@host != '') and | ||||
(!host.ends_with?('.' + @host)) and | ||||
(host != @host)) | ||||
return false | ||||
end | ||||
if path != @path | ||||
path_len = @path.length | ||||
trust_prefix = @path[0...path_len] | ||||
url_prefix = path[0...path_len] | ||||
# must be equal up to the length of the path, at least | ||||
if trust_prefix != url_prefix | ||||
return false | ||||
end | ||||
# These characters must be on the boundary between the end | ||||
# of the trust root's path and the start of the URL's path. | ||||
if !@path.index('?').nil? | ||||
allowed = '&' | ||||
else | ||||
allowed = '?/' | ||||
end | ||||
return (!allowed.index(@path[-1]).nil? or | ||||
!allowed.index(path[path_len]).nil?) | ||||
end | ||||
return true | ||||
end | ||||
end | ||||
end | ||||
end | ||||