239 lines
6.5 KiB
Ruby
239 lines
6.5 KiB
Ruby
|
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
|