require 'net/http' require 'openid' require 'openid/util' begin require 'net/https' rescue LoadError OpenID::Util.log('WARNING: no SSL support found. Will not be able ' + 'to fetch HTTPS URLs!') require 'net/http' end MAX_RESPONSE_KB = 1024 module Net class HTTP def post_connection_check(hostname) check_common_name = true cert = @socket.io.peer_cert cert.extensions.each { |ext| next if ext.oid != "subjectAltName" ext.value.split(/,\s+/).each{ |general_name| if /\ADNS:(.*)/ =~ general_name check_common_name = false reg = Regexp.escape($1).gsub(/\\\*/, "[^.]+") return true if /\A#{reg}\z/i =~ hostname elsif /\AIP Address:(.*)/ =~ general_name check_common_name = false return true if $1 == hostname end } } if check_common_name cert.subject.to_a.each{ |oid, value| if oid == "CN" reg = Regexp.escape(value).gsub(/\\\*/, "[^.]+") return true if /\A#{reg}\z/i =~ hostname end } end raise OpenSSL::SSL::SSLError, "hostname does not match" end end end module OpenID # Our HTTPResponse class extends Net::HTTPResponse with an additional # method, final_url. class HTTPResponse attr_accessor :final_url attr_accessor :_response def self._from_net_response(response, final_url, headers=nil) me = self.new me._response = response me.final_url = final_url return me end def method_missing(method, *args) @_response.send(method, *args) end def body=(s) @_response.instance_variable_set('@body', s) # XXX Hack to work around ruby's HTTP library behavior. @body # is only returned if it has been read from the response # object's socket, but since we're not using a socket in this # case, we need to set the @read flag to true to avoid a bug in # Net::HTTPResponse.stream_check when @socket is nil. @_response.instance_variable_set('@read', true) end end class FetchingError < OpenIDError end class HTTPRedirectLimitReached < FetchingError end class SSLFetchingError < FetchingError end @fetcher = nil def self.fetch(url, body=nil, headers=nil, redirect_limit=StandardFetcher::REDIRECT_LIMIT) return fetcher.fetch(url, body, headers, redirect_limit) end def self.fetcher if @fetcher.nil? @fetcher = StandardFetcher.new end return @fetcher end def self.fetcher=(fetcher) @fetcher = fetcher end # Set the default fetcher to use the HTTP proxy defined in the environment # variable 'http_proxy'. def self.fetcher_use_env_http_proxy proxy_string = ENV['http_proxy'] return unless proxy_string proxy_uri = URI.parse(proxy_string) @fetcher = StandardFetcher.new(proxy_uri.host, proxy_uri.port, proxy_uri.user, proxy_uri.password) end class StandardFetcher USER_AGENT = "ruby-openid/#{OpenID::VERSION} (#{RUBY_PLATFORM})" REDIRECT_LIMIT = 5 TIMEOUT = 60 attr_accessor :ca_file attr_accessor :timeout # I can fetch through a HTTP proxy; arguments are as for Net::HTTP::Proxy. def initialize(proxy_addr=nil, proxy_port=nil, proxy_user=nil, proxy_pass=nil) @ca_file = nil @proxy = Net::HTTP::Proxy(proxy_addr, proxy_port, proxy_user, proxy_pass) @timeout = TIMEOUT end def supports_ssl?(conn) return conn.respond_to?(:use_ssl=) end def make_http(uri) http = @proxy.new(uri.host, uri.port) http.read_timeout = @timeout http.open_timeout = @timeout return http end def set_verified(conn, verify) if verify conn.verify_mode = OpenSSL::SSL::VERIFY_PEER else conn.verify_mode = OpenSSL::SSL::VERIFY_NONE end end def make_connection(uri) conn = make_http(uri) if !conn.is_a?(Net::HTTP) raise RuntimeError, sprintf("Expected Net::HTTP object from make_http; got %s", conn.class) end if uri.scheme == 'https' if supports_ssl?(conn) conn.use_ssl = true if @ca_file set_verified(conn, true) conn.ca_file = @ca_file else Util.log("WARNING: making https request to #{uri} without verifying " + "server certificate; no CA path was specified.") set_verified(conn, false) end else raise RuntimeError, "SSL support not found; cannot fetch #{uri}" end end return conn end def fetch(url, body=nil, headers=nil, redirect_limit=REDIRECT_LIMIT) unparsed_url = url.dup url = URI::parse(url) if url.nil? raise FetchingError, "Invalid URL: #{unparsed_url}" end headers ||= {} headers['User-agent'] ||= USER_AGENT begin conn = make_connection(url) response = nil response = conn.start { # Check the certificate against the URL's hostname if supports_ssl?(conn) and conn.use_ssl? conn.post_connection_check(url.host) end if body.nil? conn.request_get(url.request_uri, headers) else headers["Content-type"] ||= "application/x-www-form-urlencoded" conn.request_post(url.request_uri, body, headers) end } rescue RuntimeError => why raise why rescue OpenSSL::SSL::SSLError => why raise SSLFetchingError, "Error connecting to SSL URL #{url}: #{why}" rescue FetchingError => why raise why rescue Exception => why # Things we've caught here include a Timeout::Error, which descends # from SignalException. raise FetchingError, "Error fetching #{url}: #{why}" end case response when Net::HTTPRedirection if redirect_limit <= 0 raise HTTPRedirectLimitReached.new( "Too many redirects, not fetching #{response['location']}") end begin return fetch(response['location'], body, headers, redirect_limit - 1) rescue HTTPRedirectLimitReached => e raise e rescue FetchingError => why raise FetchingError, "Error encountered in redirect from #{url}: #{why}" end else return HTTPResponse._from_net_response(response, unparsed_url) end end end end