# @(#) MQMBID sn=mqkoa-L160208.09 su=_Zdh2gM49EeWAYJom138ZUQ pn=appmsging/ruby/mqlight/lib/mqlight/util.rb
#
#
# Licensed Materials - Property of IBM
#
# 5725-P60
#
# (C) Copyright IBM Corp. 2014,2016
#
# US Government Users Restricted Rights - Use, duplication or
# disclosure restricted by GSA ADP Schedule Contract with
# IBM Corp.
#
require 'uri'
require 'net/http'
require 'json'
module Mqlight
#
#
#
# @private
class Util
include Mqlight::Logging
def self.validate_services(service, property_user, property_pass)
Logging.logger.entry(@id) { self.class.to_s + '#' + __method__.to_s }
parms = Hash[method(__method__).parameters.map do |parm|
[parm[1], eval(parm[1].to_s)]
end]
Logging.logger.parms(@id, parms) do
self.class.to_s + '#' + __method__.to_s
end
property_auth = nil
if property_user && property_pass
property_auth = "#{URI.encode_www_form_component(property_user)}:"\
"#{URI.encode_www_form_component(property_pass)}"
end
service_strings = []
# Convert argument into an array
if service.is_a?(Array)
service_strings = service
elsif service.is_a?(String)
service_strings << service
end
service_uris = []
# For each entry convert to URI and validate.
service_strings.each do |s|
begin
uri = URI(s)
rescue
raise ArgumentError, "#{s} is not a valid service"
end
fail ArgumentError, "#{s} is not a valid service" if uri.host.nil?
next if uri.scheme.eql?('http') || uri.scheme.eql?('https')
fail ArgumentError, "#{s} is not a supported scheme" \
unless uri.scheme.eql?('amqp') || uri.scheme.eql?('amqps')
if uri.userinfo
fail ArgumentError,
"URLs supplied via the 'service' property must specify both a "\
'user name and a password value, or omit both values' unless
uri.userinfo.split(':').size == 2
fail ArgumentError,
"User name supplied as an argument (#{property_auth}) does not"\
' match user name supplied via a service url'\
"(#{uri.userinfo})" if
property_auth && !(property_auth.eql? uri.userinfo)
end
fail ArgumentError,
"One of the supplied services (#{uri}) #{uri.path} " \
'is not a valid URL' \
unless uri.path.nil? || uri.path.length == 0 \
|| uri.path == '/'
service_uris << uri
end
Logging.logger.exit(@id, [service_uris]) \
{ self.class.to_s + '#' + __method__.to_s }
return service_uris
end
def self.generate_services(service, property_user, property_pass)
Logging.logger.entry(@id) { self.class.to_s + '#' + __method__.to_s }
parms = Hash[method(__method__).parameters.map do |parm|
[parm[1], eval(parm[1].to_s)]
end]
Logging.logger.parms(@id, parms) do
self.class.to_s + '#' + __method__.to_s
end
# if 'service' param is an http(s) URI then fetch the service list from it
if service.is_a?(String)
uri = URI(service)
if uri.scheme.eql?('http') || uri.scheme.eql?('https')
service = get_service_urls(uri)
end
end
service_uris = validate_services(service, property_user, property_pass)
Logging.logger.exit(@id, [service_uris]) \
{ self.class.to_s + '#' + __method__.to_s }
return service_uris
end
#
def self.get_service_urls(lookup_uri)
Logging.logger.entry(@id) { self.class.to_s + '#' + __method__.to_s }
parms = Hash[method(__method__).parameters.map do |parm|
[parm[1], eval(parm[1].to_s)]
end]
Logging.logger.parms(@id, parms) do
self.class.to_s + '#' + __method__.to_s
end
fail ArgumentError, 'lookup_uri must be a String or URI' unless
(lookup_uri.is_a?(String)) || (lookup_uri.is_a?(URI))
res = http_get(URI(lookup_uri))
fail Mqlight::NetworkError, "http request to #{lookup_uri} failed "\
"with status code of #{res.code}" unless res.code == '200'
result = JSON.parse(res.body)['service']
Logging.logger.exit(@id, result) \
{ self.class.to_s + '#' + __method__.to_s }
result
rescue => e
Logging.logger.throw(nil, e) { self.class.to_s + '#' + __method__.to_s }
raise e
end
# @private
def self.http_get(lookup_uri)
Logging.logger.entry(@id) { self.class.to_s + '#' + __method__.to_s }
parms = Hash[method(__method__).parameters.map do |parm|
[parm[1], eval(parm[1].to_s)]
end]
Logging.logger.parms(@id, parms) do
self.class.to_s + '#' + __method__.to_s
end
validate_uri_scheme(lookup_uri)
Net::HTTP.start(lookup_uri.host, lookup_uri.port,
use_ssl: (lookup_uri.scheme == 'https')) do |http|
path = lookup_uri.path
path += '?' + lookup_uri.query if lookup_uri.query
get = Net::HTTP::Get.new(path)
http.request(get)
end
rescue => e
Logging.logger.throw(nil, e) { self.class.to_s + '#' + __method__.to_s }
raise ArgumentError, "Could not access lookup details because #{e}"
end
# @private
def self.validate_uri_scheme(lookup_uri)
fail ArgumentError, 'lookup_uri must be a http or https URI.' unless
(lookup_uri.scheme.eql? 'http') || (lookup_uri.scheme.eql? 'https')
end
#
#
#
def self.truncate(text)
text
text[0..200] + '... (truncated from ' + text.length.to_s + ')' \
if text.length > 200
end
end # End of class
#
# A contain design to hold all the connection information.
# Note. the 'to_s' has been designed to supress showing the password.
#
class Service
include Mqlight::Logging
include URI
attr_reader :pattern
attr_reader :address
attr_reader :service
attr_reader :host
attr_reader :port
#
# @param service [URI] of the service to connect to
# @param user [String] the user id to connect with
# @param password [String] the password for the given user id.
#
def initialize(uri, user = nil, password = nil)
# No Trace - security
@service_uri = uri
unless @service_uri.port
@service_uri.port = (@service_uri.scheme == 'amqps') ? 5671 : 5672
end
# Handle authentication information.
if user && password && @service_uri.userinfo.nil?
@service_uri.userinfo = "#{URI.encode_www_form_component(user)}:"\
"#{URI.encode_www_form_component(password)}"
end
@address = @service_uri.to_s
p = @service_uri.clone
p.userinfo = ''
@pattern = p.to_s
@service = "#{@service_uri.scheme}://#{@service_uri.host}:" \
"#{@service_uri.port}"
@host = @service_uri.host
@port = @service_uri.port
# No Trace
end
#
#
#
def ssl?
@service_uri.scheme == 'amqps'
end
#
#
#
def to_s
if @service_uri.userinfo
"[Service] #{@service_uri.scheme}://#{@service_uri.user}:*******" \
"@#{@service_uri.host}:#{@service_uri.port}"
else
"[Service] #{@service_uri.scheme}://#{@service_uri.host}:" \
"#{@service_uri.port}"
end
end
#
# Override inspect so that the URI passwords are not returned
# as clear text
#
def inspect
a = ''
end
end # End of class
#
# This class handles and processes the
# SSL connection options for this client.
#
class SecureSocket
include Mqlight::Logging
attr_reader :ssl_trust_certificate
attr_reader :verified_host_name
#
# @params [] all SSL arguments
#
def initialize(options)
@id = options.fetch(:id, nil)
logger.entry(@id) { self.class.to_s + '#' + __method__.to_s }
parms = Hash[method(__method__).parameters.map do |parm|
[parm[1], eval(parm[1].to_s)]
end]
Logging.logger.parms(@id, parms) do
self.class.to_s + '#' + __method__.to_s
end
# Look for any non keystore argument and remember first
first_with_keystore_argument = nil
without_keystore_options = \
[:ssl_client_certificate, :ssl_trust_certificate,
:ssl_client_key, :ssl_client_key_passphrase]
with_keystore_options = [:ssl_keystore, :ssl_keystore_passphrase]
with_keystore_options.each do |argument_name|
unless options[argument_name].nil?
first_with_keystore_argument = argument_name
break
end
end
# Look for any keystore argument and remember first
first_without_keystore_argument = nil
without_keystore_options.each do |argument_name|
unless options[argument_name].nil?
first_without_keystore_argument = argument_name
break
end
end
fail ArgumentError, 'Invalid combination of arguments '\
"#{first_without_keystore_argument} and " \
"#{first_with_keystore_argument}" \
if first_without_keystore_argument && first_with_keystore_argument
# Load and validate arguments
if first_without_keystore_argument
# Load options
@ssl_client_certificate = options[:ssl_client_certificate]
@ssl_trust_certificate = options[:ssl_trust_certificate]
@ssl_client_key = options[:ssl_client_key]
@ssl_client_key_passphrase = options[:ssl_client_key_passphrase]
# Validate types
fail ArgumentError, 'ssl_client_certificate must be of type String' \
unless @ssl_client_certificate.nil? ||
@ssl_client_certificate.is_a?(String)
fail ArgumentError, 'ssl_trust_certificate must be of type String' \
unless @ssl_trust_certificate.nil? ||
@ssl_trust_certificate.is_a?(String)
fail ArgumentError, 'ssl_client_key must be of type String' \
unless @ssl_client_key.nil? ||
@ssl_client_key.is_a?(String)
fail ArgumentError, 'ssl_client_key_passphrase must be of type String' \
unless @ssl_client_key_passphrase.nil? ||
@ssl_client_key_passphrase.is_a?(String)
# Combination check.
fail ArgumentError,
'Invalid combination of arguments. The client key passphrase is ' \
'present but no associated client key has been specified' \
if !@ssl_client_key_passphrase.nil? && @ssl_client_key.nil?
# client key set : If one is present then all must be present
# sslClientCertificate, sslClientKey and sslClientKeyPassphrase
client_key_set_one_present = !@ssl_client_certificate.nil? || !@ssl_client_key.nil? || !@ssl_client_key_passphrase.nil?
client_key_set_one_missing = @ssl_client_certificate.nil? || @ssl_client_key.nil? || @ssl_client_key_passphrase.nil?
fail ArgumentError,
'sslClientCertificate, sslClientKey and sslClientKeyPassphrase ' \
'options must all be specified' \
if client_key_set_one_present && client_key_set_one_missing
# Check file references
validate_file_path @ssl_trust_certificate, 'ssl_trust_certificate' \
unless @ssl_trust_certificate.nil?
validate_file_path @ssl_client_certificate, 'ssl_client_certificate'\
unless @ssl_client_certificate.nil?
validate_file_path @ssl_client_key, 'ssl_client_key'\
unless @ssl_client_key.nil?
@keystore_present = false
elsif first_with_keystore_argument
# Load options
ssl_keystore = options[:ssl_keystore]
ssl_keystore_passphrase = options[:ssl_keystore_passphrase]
# Validate types
fail ArgumentError, 'ssl_keystore must be of type String' \
unless ssl_keystore.is_a? String
fail ArgumentError, 'ssl_keystore_passphrase must be of type String' \
unless ssl_keystore_passphrase.is_a? String
# Combination check
fail ArgumentError,
'Invalid combination of arguments. The keystore passphrase is ' \
'present but no associated keystore has been specified' \
if ssl_keystore_passphrase.nil? && !ssl_keystore.nil?
# Check file references
validate_file_path ssl_keystore, 'ssl_keystore' \
unless ssl_keystore.nil?
# Load the keystore
data = File.binread(ssl_keystore)
@keystore_pkcs12 = OpenSSL::PKCS12.new(data, ssl_keystore_passphrase)
@keystore_present = true
end
# If server host verification option required?
@ssl_verify_name = options.fetch(:ssl_verify_name, false)
fail ArgumentError, 'ssl_verify_name must be of type Binary' \
unless @ssl_verify_name.is_a? TrueClass or
@ssl_verify_name.is_a? FalseClass
logger.exit(@id) { self.class.to_s + '#' + __method__.to_s }
rescue StandardError => e
logger.throw(@id, e) { self.class.to_s + '#' + __method__.to_s }
raise e
end
#
# @return [PKey] containing the given private key or nil if none is present
#
def rsa_private_key
logger.entry(@id) { self.class.to_s + '#' + __method__.to_s }
rc = nil
if @keystore_present
rc = @keystore_pkcs12.key
elsif !@ssl_client_key.nil?
begin
rc = OpenSSL::PKey::RSA.new \
File.read(@ssl_client_key), \
@ssl_client_key_passphrase
rescue OpenSSL::PKey::RSAError => re
logger.throw(@id, re) { self.class.to_s + '#' + __method__.to_s }
fail ArgumentError, \
'File given for the ssl_client_key is not a valid RSA ' \
'Certificate'
end
end
logger.exit(@id) { self.class.to_s + '#' + __method__.to_s }
rc
end
#
# @return [X509Store] contains one or more CA for the given keystore
# or arguments. nil is returned is none are present
#
def x509_certificate_authorities
logger.entry(@id) { self.class.to_s + '#' + __method__.to_s }
store = nil
if @keystore_present && !@keystore_pkcs12.ca_certs.nil?
store = OpenSSL::X509::Store.new
@keystore_pkcs12.ca_certs.each do |cert|
begin
store.add_cert cert
rescue OpenSSL::X509::StoreError => e
fail ArgumentError, \
'File given for the \'ssl_trust_certificate\' argument ' \
"has the following error \'#{e}\'" \
unless e.message.include? 'cert already in hash table'
logger.data(@id, e.message + ' - Certificate has been skipped') do
self.class.to_s + '#' + __method__.to_s
end
end
end
elsif ! @ssl_trust_certificate.nil?
begin
store = OpenSSL::X509::Store.new
store.add_file @ssl_trust_certificate
rescue OpenSSL::X509::StoreError => se
logger.throw(@id, se) { self.class.to_s + '#' + __method__.to_s }
fail ArgumentError, \
'File given for the \'ssl_trust_certificate\' argument is not ' \
'a valid trust certificate'
end
else
store = OpenSSL::X509::Store.new
store.set_default_paths
end
logger.exit(@id) { self.class.to_s + '#' + __method__.to_s }
store
end
#
# @return [OpenSSL::X509::Certificate] from the given attributes or
# null if not are present.
#
def x509_certificate
logger.entry(@id) { self.class.to_s + '#' + __method__.to_s }
if @keystore_present
rc = @keystore_pkcs12.certificate
elsif ! @ssl_client_certificate.nil?
begin
rc = OpenSSL::X509::Certificate.new File.read(@ssl_client_certificate)
rescue OpenSSL::X509::CertificateError => se
logger.throw(@id, se) { self.class.to_s + '#' + __method__.to_s }
fail ArgumentError,
'File given for the \'ssl_client_certificate\' argument ' \
'is not a valid X509 certificate'
end
end
logger.exit(@id) { self.class.to_s + '#' + __method__.to_s }
rc
end
#
# Create the SSL context based on the given attributes
# @param server_host_name [String] name of the server host to be used for
# validating certificates.
# @return [SSLContext] the generated context
#
def context(server_host_name)
logger.entry(@id) { self.class.to_s + '#' + __method__.to_s }
parms = Hash[method(__method__).parameters.map do |parm|
[parm[1], eval(parm[1].to_s)]
end]
logger.parms(@id, parms) { self.class.to_s + '#' + __method__.to_s }
context = OpenSSL::SSL::SSLContext.new
# Create the X509 Store for the CAs
context.cert_store = x509_certificate_authorities
# Private key
context.key = rsa_private_key
# Client Certificate
context.cert = x509_certificate
context.verify_mode = OpenSSL::SSL::VERIFY_PEER
# If verify name required then assign callback facility to
# receive acknowledgement
@verified_host_name = false
if @ssl_verify_name
context.verify_callback = proc do |_preverify, inner_context|
@verified_host_name |= OpenSSL::SSL.verify_certificate_identity(
inner_context.current_cert, server_host_name)
true
end
end
logger.exit(@id, context) { self.class.to_s + '#' + __method__.to_s }
context
end
# Validates that the given file is present and regular
# @param file_path [String] file path to file to be validate
# @param file_description [String] descriptive file of file.
def validate_file_path(file_path, file_description)
fail ArgumentError,
"The file specified for #{file_description} does not exist" \
unless File.exist?(file_path)
fail ArgumentError,
"The file specified for #{file_description} is not a regular file" \
unless File.file?(file_path)
end
#
# @return [Boolean] true indicate verification was required and it
# failed.
#
def verify_server_host_name_failed?
@ssl_verify_name && !@verified_host_name
end
end
end