Skip to content

Instantly share code, notes, and snippets.

@suryart
Forked from dreamwords/gist:149518
Created March 26, 2019 19:19
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save suryart/4db3482d5f5fc47b7722dfdd6b5b657e to your computer and use it in GitHub Desktop.
activeresource client certificate
diff --git a/activeresource/lib/active_resource/base.rb b/activeresource/lib/active_resource/base.rb
index dc24e71..f814b8b 100644
--- a/activeresource/lib/active_resource/base.rb
+++ b/activeresource/lib/active_resource/base.rb
@@ -102,6 +102,8 @@ module ActiveResource
#
# Many REST APIs will require authentication, usually in the form of basic
# HTTP authentication. Authentication can be specified by:
+ #
+ # === HTTP Basic Authentication
# * putting the credentials in the URL for the +site+ variable.
#
# class Person < ActiveResource::Base
@@ -122,6 +124,19 @@ module ActiveResource
# Note: Some values cannot be provided in the URL passed to site. e.g. email addresses
# as usernames. In those situations you should use the separate user and password option.
#
+ # === Certificate Authentication
+ #
+ # * End point uses an X509 certificate for authentication. <tt>See ssl_options=</tt> for all options.
+ #
+ # class Person < ActiveResource::Base
+ # self.site = "https://secure.api.people.com/"
+ # self.ssl_options = {:cert => OpenSSL::X509::Certificate.new(File.open(pem_file))
+ # :key => OpenSSL::PKey::RSA.new(File.open(pem_file)),
+ # :ca_path => "/path/to/OpenSSL/formatted/CA_Certs",
+ # :verify_mode => OpenSSL::SSL::VERIFY_PEER}
+ # end
+ #
+ #
# == Errors & Validation
#
# Error handling and validation is handled in much the same manner as you're used to seeing in
@@ -325,6 +340,31 @@ module ActiveResource
end
end
+ # Options that will get applied to an SSL connection.
+ #
+ # * <tt>:key</tt> - An OpenSSL::PKey::RSA or OpenSSL::PKey::DSA object.
+ # * <tt>:cert</tt> - An OpenSSL::X509::Certificate object as client certificate
+ # * <tt>:ca_file</tt> - Path to a CA certification file in PEM format. The file can contrain several CA certificates.
+ # * <tt>:ca_path</tt> - Path of a CA certification directory containing certifications in PEM format.
+ # * <tt>:verify_mode</tt> - Flags for server the certification verification at begining of SSL/TLS session. (OpenSSL::SSL::VERIFY_NONE or OpenSSL::SSL::VERIFY_PEER is acceptable)
+ # * <tt>:verify_callback</tt> - The verify callback for the server certification verification.
+ # * <tt>:verify_depth</tt> - The maximum depth for the certificate chain verification.
+ # * <tt>:cert_store</tt> - OpenSSL::X509::Store to verify peer certificate.
+ # * <tt>:ssl_timeout</tt> -The SSL timeout in seconds.
+ def ssl_options=(opts={})
+ @connection = nil
+ @ssl_options = opts
+ end
+
+ # Returns the SSL options hash.
+ def ssl_options
+ if defined?(@ssl_options)
+ @ssl_options
+ elsif superclass != Object && superclass.ssl_options
+ superclass.ssl_options
+ end
+ end
+
# An instance of ActiveResource::Connection that is the base \connection to the remote service.
# The +refresh+ parameter toggles whether or not the \connection is refreshed at every request
# or not (defaults to <tt>false</tt>).
@@ -334,6 +374,7 @@ module ActiveResource
@connection.user = user if user
@connection.password = password if password
@connection.timeout = timeout if timeout
+ @connection.ssl_options = ssl_options if ssl_options
@connection
else
superclass.connection
diff --git a/activeresource/lib/active_resource/connection.rb b/activeresource/lib/active_resource/connection.rb
index 6661469..f9b73c5 100644
--- a/activeresource/lib/active_resource/connection.rb
+++ b/activeresource/lib/active_resource/connection.rb
@@ -18,7 +18,7 @@ module ActiveResource
:delete => 'Accept'
}
- attr_reader :site, :user, :password, :timeout
+ attr_reader :site, :user, :password, :timeout, :ssl_options
attr_accessor :format
class << self
@@ -58,6 +58,11 @@ module ActiveResource
@timeout = timeout
end
+ # Hash of options applied to Net::HTTP instance when +site+ protocol is 'https'.
+ def ssl_options=(opts={})
+ @ssl_options = opts
+ end
+
# Executes a GET request.
# Used to get (find) resources.
def get(path, headers = {})
@@ -94,11 +99,15 @@ module ActiveResource
def request(method, path, *arguments)
logger.info "#{method.to_s.upcase} #{site.scheme}://#{site.host}:#{site.port}#{path}" if logger
result = nil
- ms = Benchmark.ms { result = http.send(method, path, *arguments) }
+ http_instance = http
+ http_instance = apply_ssl_options(http_instance) if site.is_a?(URI::HTTPS)
+ ms = Benchmark.ms { result = http_instance.send(method, path, *arguments) }
logger.info "--> %d %s (%d %.0fms)" % [result.code, result.message, result.body ? result.body.length : 0, ms] if logger
handle_response(result)
rescue Timeout::Error => e
raise TimeoutError.new(e.message)
+ rescue OpenSSL::SSL::SSLError => e
+ raise SSLError.new(e.message)
end
# Handles response and error codes from the remote service.
@@ -134,13 +143,32 @@ module ActiveResource
# Creates new Net::HTTP instance for communication with the
# remote service and resources.
def http
- http = Net::HTTP.new(@site.host, @site.port)
- http.use_ssl = @site.is_a?(URI::HTTPS)
- http.verify_mode = OpenSSL::SSL::VERIFY_NONE if http.use_ssl
+ http = Net::HTTP.new(@site.host, @site.port)
http.read_timeout = @timeout if @timeout # If timeout is not set, the default Net::HTTP timeout (60s) is used.
http
end
+ def apply_ssl_options(http)
+ http.use_ssl = true
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
+ return http unless defined?(@ssl_options)
+
+ http.ca_path = @ssl_options[:ca_path] if @ssl_options[:ca_path]
+ http.ca_file = @ssl_options[:ca_file] if @ssl_options[:ca_file]
+
+ http.cert = @ssl_options[:cert] if @ssl_options[:cert]
+ http.key = @ssl_options[:key] if @ssl_options[:key]
+
+ http.cert_store = @ssl_options[:cert_store] if @ssl_options[:cert_store]
+ http.ssl_timeout = @ssl_options[:ssl_timeout] if @ssl_options[:ssl_timeout]
+
+ http.verify_mode = @ssl_options[:verify_mode] if @ssl_options[:verify_mode]
+ http.verify_callback = @ssl_options[:verify_callback] if @ssl_options[:verify_callback]
+ http.verify_depth = @ssl_options[:verify_depth] if @ssl_options[:verify_depth]
+
+ http
+ end
+
def default_header
@default_header ||= {}
end
diff --git a/activeresource/lib/active_resource/exceptions.rb b/activeresource/lib/active_resource/exceptions.rb
index 5e4b1d4..dd59146 100644
--- a/activeresource/lib/active_resource/exceptions.rb
+++ b/activeresource/lib/active_resource/exceptions.rb
@@ -20,6 +20,14 @@ module ActiveResource
def to_s; @message ;end
end
+ # Raised when a OpenSSL::SSL::SSLError occurs.
+ class SSLError < ConnectionError
+ def initialize(message)
+ @message = message
+ end
+ def to_s; @message ;end
+ end
+
# 3xx Redirection
class Redirection < ConnectionError # :nodoc:
def to_s; response['Location'] ? "#{super} => #{response['Location']}" : super; end
diff --git a/activeresource/test/base_test.rb b/activeresource/test/base_test.rb
index 82d3b2a..6b0d666 100644
--- a/activeresource/test/base_test.rb
+++ b/activeresource/test/base_test.rb
@@ -143,6 +143,13 @@ class BaseTest < Test::Unit::TestCase
assert_equal(5, Forum.connection.timeout)
end
+ def test_should_accept_setting_ssl_options
+ expected = {:verify => 1}
+ Forum.ssl_options= expected
+ assert_equal(expected, Forum.ssl_options)
+ assert_equal(expected, Forum.connection.ssl_options)
+ end
+
def test_user_variable_can_be_reset
actor = Class.new(ActiveResource::Base)
actor.site = 'http://cinema'
@@ -173,6 +180,16 @@ class BaseTest < Test::Unit::TestCase
assert_nil actor.connection.timeout
end
+ def test_ssl_options_hash_can_be_reset
+ actor = Class.new(ActiveResource::Base)
+ actor.site = 'https://cinema'
+ assert_nil actor.ssl_options
+ actor.ssl_options = {:foo => 5}
+ actor.ssl_options = nil
+ assert_nil actor.ssl_options
+ assert_nil actor.connection.ssl_options
+ end
+
def test_credentials_from_site_are_decoded
actor = Class.new(ActiveResource::Base)
actor.site = 'http://my%40email.com:%31%32%33@cinema'
@@ -331,6 +348,40 @@ class BaseTest < Test::Unit::TestCase
assert_equal fruit.timeout, apple.timeout, 'subclass did not adopt changes from parent class'
end
+ def test_ssl_options_reader_uses_superclass_ssl_options_until_written
+ # Superclass is Object so returns nil.
+ assert_nil ActiveResource::Base.ssl_options
+ assert_nil Class.new(ActiveResource::Base).ssl_options
+ Person.ssl_options = {:foo => 'bar'}
+
+ # Subclass uses superclass ssl_options.
+ actor = Class.new(Person)
+ assert_equal Person.ssl_options, actor.ssl_options
+
+ # Changing subclass ssl_options doesn't change superclass ssl_options.
+ actor.ssl_options = {:baz => ''}
+ assert_not_equal Person.ssl_options, actor.ssl_options
+
+ # Changing superclass ssl_options doesn't overwrite subclass ssl_options.
+ Person.ssl_options = {:color => 'blue'}
+ assert_not_equal Person.ssl_options, actor.ssl_options
+
+ # Changing superclass ssl_options after subclassing changes subclass ssl_options.
+ jester = Class.new(actor)
+ actor.ssl_options = {:color => 'red'}
+ assert_equal actor.ssl_options, jester.ssl_options
+
+ # Subclasses are always equal to superclass ssl_options when not overridden.
+ fruit = Class.new(ActiveResource::Base)
+ apple = Class.new(fruit)
+
+ fruit.ssl_options = {:alpha => 'betas'}
+ assert_equal fruit.ssl_options, apple.ssl_options, 'subclass did not adopt changes from parent class'
+
+ fruit.ssl_options = {:omega => 'moos'}
+ assert_equal fruit.ssl_options, apple.ssl_options, 'subclass did not adopt changes from parent class'
+ end
+
def test_updating_baseclass_site_object_wipes_descendent_cached_connection_objects
# Subclasses are always equal to superclass site when not overridden
fruit = Class.new(ActiveResource::Base)
diff --git a/activeresource/test/connection_test.rb b/activeresource/test/connection_test.rb
index 831fbc4..7e016c0 100644
--- a/activeresource/test/connection_test.rb
+++ b/activeresource/test/connection_test.rb
@@ -183,6 +183,26 @@ class ConnectionTest < Test::Unit::TestCase
assert_nothing_raised(Mocha::ExpectationError) { @conn.get(path, {'Accept' => 'application/xhtml+xml'}) }
end
+ def test_ssl_options_get_applied_to_http
+ @http = mock('new Net::HTTP')
+ @conn.site="https://secure"
+ @conn.ssl_options={:verify_mode => OpenSSL::SSL::VERIFY_PEER}
+ @conn.expects(:http).returns(@http)
+ path = '/people/1.xml'
+ @http.expects(:get).with(path, {'Accept' => 'application/xhtml+xml'}).returns(ActiveResource::Response.new(@matz, 200, {'Content-Type' => 'text/xhtml'}))
+ @http.expects(:use_ssl=).with(true)
+ @http.expects(:verify_mode=).with(OpenSSL::SSL::VERIFY_NONE) # The default
+ @http.expects(:verify_mode=).with(OpenSSL::SSL::VERIFY_PEER)
+ assert_nothing_raised(Mocha::ExpectationError) { @conn.get(path, {'Accept' => 'application/xhtml+xml'}) }
+ end
+
+ def test_ssl_error
+ @http = mock('new Net::HTTP')
+ @conn.expects(:http).returns(@http)
+ @http.expects(:get).raises(OpenSSL::SSL::SSLError, 'Expired certificate')
+ assert_raise(ActiveResource::SSLError) { @conn.get('/people/1.xml') }
+ end
+
protected
def assert_response_raises(klass, code)
assert_raise(klass, "Expected response code #{code} to raise #{klass}") do
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment