Skip to content

Commit 45ddb19

Browse files
authored
Merge pull request #141 from CyberSource/features-ready-to-release
Features ready to release
2 parents af7c1ba + d319fa6 commit 45ddb19

File tree

11 files changed

+199
-74
lines changed

11 files changed

+199
-74
lines changed

cybersource_rest_client.gemspec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ require "cybersource_rest_client/version"
1717

1818
Gem::Specification.new do |s|
1919
s.name = "cybersource_rest_client"
20-
s.version = "0.0.75"
20+
s.version = "0.0.76"
2121
s.platform = Gem::Platform::RUBY
2222
s.authors = ["CyberSource"]
2323
s.email = ["cybersourcedev@gmail.com"]

generator/cybersource-ruby-template/api_client.mustache

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ require 'logger'
88
require 'tempfile'
99
require 'typhoeus'
1010
require 'addressable/uri'
11+
require_relative 'ethon_extensions'
1112

1213
module {{moduleName}}
1314
class ApiClient
@@ -144,7 +145,8 @@ module {{moduleName}}
144145
:sslcert => @config.cert_file,
145146
:sslkeypasswd => @merchantconfig.sslKeyPassword || "",
146147
:sslkey => @config.key_file,
147-
:verbose => @config.debugging
148+
:verbose => @config.debugging,
149+
:maxage_conn => @merchantconfig.keepAliveTime || 118 # Default to 118 seconds as same as default of libcurl
148150
}
149151

150152
# set custom cert, if provided

lib/AuthenticationSDK/authentication/jwt/JwtToken.rb

Lines changed: 6 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -21,29 +21,18 @@ def getToken(merchantconfig_obj,gmtDatetime)
2121

2222
jwtBody = ''
2323
request_type = merchantconfig_obj.requestType.upcase
24-
filePath = merchantconfig_obj.keysDirectory + '/' + merchantconfig_obj.keyFilename + '.p12'
25-
26-
if (!File.exist?(filePath))
27-
raise Constants::ERROR_PREFIX + Constants::FILE_NOT_FOUND + File.expand_path(filePath)
28-
end
29-
30-
p12File = File.binread(filePath)
24+
3125
jwtBody=getJwtBody(request_type, gmtDatetime, merchantconfig_obj)
3226
claimSet = JSON.parse(jwtBody)
33-
p12FilePath = OpenSSL::PKCS12.new(p12File, merchantconfig_obj.keyPass)
34-
35-
# Generating certificate.
36-
cacheObj = ActiveSupport::Cache::MemoryStore.new
37-
x5Cert = Cache.new.fetchCachedCertificate(filePath, p12File, merchantconfig_obj.keyPass, merchantconfig_obj.keyAlias, cacheObj)
3827

39-
# Generating Public key.
40-
publicKey = OpenSSL::PKey::RSA.new(p12FilePath.key.public_key)
28+
cache_value = Cache.new.fetchJwtCertsAndKeys(merchantconfig_obj)
29+
privateKey = cache_value.private_key
30+
jwt_cert_obj = cache_value.cert
31+
jwt_cert_in_der= Base64.strict_encode64(jwt_cert_obj.to_der)
4132

42-
#Generating Private Key
43-
privateKey = OpenSSL::PKey::RSA.new(p12FilePath.key)
4433

4534
# JWT token-Generates using RS256 algorithm only
46-
x5clist = [x5Cert]
35+
x5clist = [jwt_cert_in_der]
4736
customHeaders = {}
4837
customHeaders['v-c-merchant-id'] = merchantconfig_obj.keyAlias
4938
customHeaders['x5c'] = x5clist

lib/AuthenticationSDK/core/MerchantConfig.rb

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ def initialize(cybsPropertyObj)
4444
@log_config = LogConfiguration.new(cybsPropertyObj['logConfiguration'])
4545
# Custom Default Headers
4646
@defaultCustomHeaders = cybsPropertyObj['defaultCustomHeaders']
47+
# Keep Alive Time for Connection Pooling
48+
@keepAliveTime = cybsPropertyObj['keepAliveTime'] || 118 # Default to 118 seconds as same as default of libcurl
4749
# Path to client JWE pem file directory
4850
@pemFileDirectory = cybsPropertyObj['pemFileDirectory']
4951
@mleKeyAlias = cybsPropertyObj['mleKeyAlias']
@@ -56,6 +58,11 @@ def initialize(cybsPropertyObj)
5658

5759
#fall back logic
5860
def validateMerchantDetails()
61+
if !@keepAliveTime.is_a?(Integer)
62+
err = StandardError.new(Constants::ERROR_PREFIX + "keepAliveTime must be an integer and in seconds")
63+
raise err
64+
end
65+
5966
logmessage = ''
6067
@log_config.validate(logmessage)
6168
@log_obj = Log.new @log_config, "MerchantConfig"
@@ -267,7 +274,7 @@ def validateMLEConfiguration
267274
end
268275
end
269276

270-
if mle_configured && !Constants::AUTH_TYPE_JWT.eql?(@authenticationType)
277+
if mle_configured && !Constants::AUTH_TYPE_JWT.eql?(@authenticationType.upcase)
271278
err = StandardError.new(Constants::ERROR_PREFIX + "MLE can only be used with JWT authentication")
272279
@log_obj.logger.error(ExceptionHandler.new.new_api_exception err)
273280
raise err
@@ -306,6 +313,7 @@ def logAllProperties(merchantPropertyObj)
306313
attr_accessor :keyFilename
307314
attr_accessor :useMetaKey
308315
attr_accessor :portfolioID
316+
attr_accessor :keepAliveTime
309317
attr_accessor :enableClientCert
310318
attr_accessor :clientCertDirectory
311319
attr_accessor :sslClientCert

lib/AuthenticationSDK/util/Cache.rb

Lines changed: 106 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,123 @@
11
require 'openssl'
22
require 'base64'
3+
require 'active_support'
4+
require 'thread'
5+
require_relative 'CacheValue'
6+
37
public
48
# P12 file certificate Cache
59
class Cache
6-
def fetchCachedCertificate(filePath, p12File, keyPass, keyAlias, cacheObj)
7-
certCache = cacheObj.read(keyAlias.to_s.upcase)
8-
cachedLastModifiedTimeStamp = cacheObj.read(keyAlias.to_s.upcase + '_LastModifiedTime')
9-
if File.exist?(filePath)
10-
currentFileLastModifiedTime = File.mtime(filePath)
11-
if certCache.to_s.empty? || cachedLastModifiedTimeStamp.to_s.empty?
12-
certificateFromP12File = getCertificate(p12File, keyPass, keyAlias, cacheObj, currentFileLastModifiedTime)
13-
return certificateFromP12File
14-
elsif currentFileLastModifiedTime > cachedLastModifiedTimeStamp
15-
# Function call to read the file and put values to new cache
16-
certificateFromP12File = getCertificate(p12File, keyPass, keyAlias, cacheObj, currentFileLastModifiedTime)
17-
return certificateFromP12File
18-
else
19-
return certCache
10+
@@cache_obj = ActiveSupport::Cache::MemoryStore.new
11+
@@mutex = Mutex.new
12+
13+
def fetchJwtCertsAndKeys(merchantConfig)
14+
merchantId = merchantConfig.merchantId
15+
filePath = merchantConfig.keysDirectory + '/' + merchantConfig.keyFilename + '.p12'
16+
if (!File.exist?(filePath))
17+
raise Constants::ERROR_PREFIX + Constants::FILE_NOT_FOUND + File.expand_path(filePath)
18+
end
19+
20+
cacheKey = merchantId.to_s + '_JWT'
21+
22+
# Thread-safe cache access with race condition protection
23+
@@mutex.synchronize do
24+
certCache = @@cache_obj.read(cacheKey)
25+
fileModTime = File.mtime(filePath)
26+
27+
if !certCache || certCache.empty? || certCache.file_modified_time != fileModTime
28+
setupCache(cacheKey, filePath, merchantConfig)
29+
certCache = @@cache_obj.read(cacheKey)
2030
end
21-
else
22-
raise Constants::ERROR_PREFIX + Constants::FILE_NOT_FOUND + filePath
31+
32+
return certCache
33+
end
34+
end
35+
36+
def fetchMLECert(merchantConfig)
37+
merchantId = merchantConfig.merchantId
38+
filePath = merchantConfig.keysDirectory + '/' + merchantConfig.keyFilename + '.p12'
39+
if (!File.exist?(filePath))
40+
raise Constants::ERROR_PREFIX + Constants::FILE_NOT_FOUND + File.expand_path(filePath)
41+
end
42+
43+
cacheKey = merchantId.to_s + '_MLE'
44+
45+
# Thread-safe cache access with race condition protection
46+
@@mutex.synchronize do
47+
certCache = @@cache_obj.read(cacheKey)
48+
fileModTime = File.mtime(filePath)
49+
50+
if !certCache || certCache.empty? || certCache.file_modified_time != fileModTime
51+
setupCache(cacheKey, filePath, merchantConfig)
52+
certCache = @@cache_obj.read(cacheKey)
53+
end
54+
55+
return certCache
2356
end
2457
end
2558

26-
def getCertificate(p12File, keyPass, keyAlias, cacheObj, currentFileLastModifiedTime)
27-
x5CertDer = Utility.new.fetchCert(keyPass, p12File, keyAlias)
28-
cacheObj.write(keyAlias.to_s.upcase, x5CertDer)
29-
cacheObj.write(keyAlias.to_s.upcase + '_LastModifiedTime', currentFileLastModifiedTime)
30-
x5CertDer
59+
def setupCache(cacheKey, filePath, merchantConfig)
60+
if(cacheKey.end_with?("_JWT"))
61+
private_key, certs = getCertsAndKeysFromP12(filePath, merchantConfig)
62+
jwt_cert = Utility.getCertBasedOnKeyAlias(certs, merchantConfig.keyAlias)
63+
currentFileLastModifiedTime = File.mtime(filePath)
64+
65+
# Create CacheValue object with all 3 parameters
66+
cache_value = CacheValue.new(private_key, jwt_cert, currentFileLastModifiedTime)
67+
68+
# Store the cache value object in cache
69+
@@cache_obj.write(cacheKey, cache_value)
70+
end
71+
if(cacheKey.end_with?("_MLE"))
72+
private_key, certs = getCertsAndKeysFromP12(filePath, merchantConfig)
73+
mle_cert = Utility.getCertBasedOnKeyAlias(certs, merchantConfig.mleKeyAlias)
74+
currentFileLastModifiedTime = File.mtime(filePath)
75+
76+
# Create CacheValue object with all 3 parameters
77+
cache_value = CacheValue.new(nil, mle_cert, currentFileLastModifiedTime)
78+
79+
# Store the cache value object in cache
80+
@@cache_obj.write(cacheKey, cache_value)
81+
end
82+
83+
end
84+
85+
def getCertsAndKeysFromP12(filePath, merchantConfig)
86+
p12File = File.binread(filePath)
87+
p12Object = OpenSSL::PKCS12.new(p12File, merchantConfig.keyPass)
88+
#Generating Private Key
89+
private_key = OpenSSL::PKey::RSA.new(p12Object.key)
90+
# Generating Public key.
91+
# publicKey = OpenSSL::PKey::RSA.new(p12Object.key.public_key)
92+
93+
# Get all certs from p12
94+
x5_cert_primary = p12Object.certificate
95+
x5_certs_others = p12Object.ca_certs
96+
certs = [x5_cert_primary]
97+
certs.concat(x5_certs_others) if x5_certs_others
98+
return [private_key, certs]
3199
end
32100

33101
# <b>DEPRECATED:</b> This method has been marked as Deprecated and will be removed in coming releases.
34-
def fetchPEMFileForNetworkTokenization(filePath, cacheObj)
102+
def fetchPEMFileForNetworkTokenization(filePath)
35103
warn("[DEPRECATED] 'fetchPEMFileForNetworkTokenization' method is deprecated and will be removed in coming releases.")
36-
pem_file_cache = cacheObj.read('privateKeyFromPEMFile')
37-
cached_pem_file_last_updated_time = cacheObj.read('cachedLastModifiedTimeOfPEMFile')
38-
if File.exist?(filePath)
39-
current_last_modified_time_of_PEM_file = File.mtime(filePath)
40-
if pem_file_cache.nil? || pem_file_cache.to_s.empty? || current_last_modified_time_of_PEM_file > cached_pem_file_last_updated_time
41-
private_key = JOSE::JWK.from_pem_file filePath
42-
cacheObj.write('privateKeyFromPEMFile', private_key)
43-
cacheObj.write('cachedLastModifiedTimeOfPEMFile', current_last_modified_time_of_PEM_file)
104+
105+
# Thread-safe cache access for deprecated method
106+
@@mutex.synchronize do
107+
pem_file_cache = @@cache_obj.read('privateKeyFromPEMFile')
108+
cached_pem_file_last_updated_time = @@cache_obj.read('cachedLastModifiedTimeOfPEMFile')
109+
110+
if File.exist?(filePath)
111+
current_last_modified_time_of_PEM_file = File.mtime(filePath)
112+
if pem_file_cache.nil? || pem_file_cache.to_s.empty? || current_last_modified_time_of_PEM_file > cached_pem_file_last_updated_time
113+
private_key = JOSE::JWK.from_pem_file filePath
114+
@@cache_obj.write('privateKeyFromPEMFile', private_key)
115+
@@cache_obj.write('cachedLastModifiedTimeOfPEMFile', current_last_modified_time_of_PEM_file)
116+
end
44117
end
118+
119+
return @@cache_obj.read('privateKeyFromPEMFile')
45120
end
46-
return cacheObj.read('privateKeyFromPEMFile')
47121
end
122+
48123
end
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Cache value object to store certificate data
2+
class CacheValue
3+
attr_accessor :private_key, :cert, :file_modified_time
4+
5+
def initialize(private_key = nil, cert = nil, file_modified_time = nil)
6+
@private_key = private_key
7+
@cert = cert
8+
@file_modified_time = file_modified_time
9+
end
10+
11+
def to_s
12+
"CacheValue(private_key: #{@private_key ? 'present' : 'nil'}, cert: #{@cert ? 'present' : 'nil'}, file_modified_time: #{@file_modified_time})"
13+
end
14+
15+
def empty?
16+
@private_key.nil? && @cert.nil? && @file_modified_time.nil?
17+
end
18+
end

lib/AuthenticationSDK/util/JWEUtility.rb

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,7 @@ class AuthJWEUtility
88
# <b>DEPRECATED:</b> This method has been marked as Deprecated and will be removed in coming releases. Use <tt>decrypt_jwe_using_private_key()</tt> instead.
99
def self.decrypt_jwe_using_pem(merchant_config, encoded_response)
1010
warn("[DEPRECATED] `decrypt_jwe_using_pem()` method is deprecated and will be removed in coming releases. Use `decrypt_jwe_using_private_key()` instead.")
11-
cache_obj = ActiveSupport::Cache::MemoryStore.new
12-
key = Cache.new.fetchPEMFileForNetworkTokenization(merchant_config.pemFileDirectory, cache_obj)
11+
key = Cache.new.fetchPEMFileForNetworkTokenization(merchant_config.pemFileDirectory)
1312
return JOSE::JWE.block_decrypt(key, encoded_response).first
1413
end
1514

lib/AuthenticationSDK/util/MLEUtility.rb

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
require_relative '../logging/log_factory.rb'
22
require 'jose'
33
require_relative './Cache'
4-
require 'active_support'
54

65
public
76
class MLEUtility
@@ -30,23 +29,22 @@ def self.encrypt_request_payload(merchant_config, request_payload)
3029
@log_obj.logger.debug('LOG_REQUEST_BEFORE_MLE: ' + request_payload)
3130

3231
begin
33-
file_path = merchant_config.keysDirectory + '/' + merchant_config.keyFilename + '.p12'
34-
p12_file = File.binread(file_path)
35-
cache_obj = ActiveSupport::Cache::MemoryStore.new
36-
cert_der = Cache.new.fetchCachedCertificate(merchant_config.keysDirectory, p12_file, merchant_config.keyPass, merchant_config.mleKeyAlias, cache_obj)
37-
if cert_der.nil?
32+
cache_value = Cache.new.fetchMLECert(merchant_config)
33+
if cache_value.nil? || cache_value.cert.nil?
3834
@log_obj.logger.error('Failed to get certificate for MLE')
3935
raise StandardError.new('Failed to get certificate for MLE')
4036
end
41-
certificate = OpenSSL::X509::Certificate.new(Base64.decode64(cert_der))
42-
validate_certificate(certificate, merchant_config.mleKeyAlias, @log_obj)
43-
serial_number = extract_serial_number_from_certificate(certificate)
37+
mle_cert_obj = cache_value.cert
38+
39+
# certificate = OpenSSL::X509::Certificate.new(Base64.decode64(cert_der))
40+
validate_certificate(mle_cert_obj, merchant_config.mleKeyAlias, @log_obj)
41+
serial_number = extract_serial_number_from_certificate(mle_cert_obj)
4442
if serial_number.nil?
4543
@log_obj.logger.error('Serial number not found in certificate for MLE')
4644
raise StandardError.new('Serial number not found in MLE certificate')
4745
end
4846

49-
jwk = JOSE::JWK.from_key(certificate.public_key)
47+
jwk = JOSE::JWK.from_key(mle_cert_obj.public_key)
5048
if jwk.nil?
5149
@log_obj.logger.error('Failed to create JWK object from public key')
5250
raise StandardError.new('Failed to create JWK object from public key')

lib/AuthenticationSDK/util/Utility.rb

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -33,19 +33,15 @@ def getResponseCodeMessage(responseCode)
3333
return tempResponseCodeMessage
3434
end
3535

36-
def fetchCert(key_pass, file, key_alias)
37-
p12_file = OpenSSL::PKCS12.new(file, key_pass)
38-
x5_cert_pem = p12_file.certificate
39-
x5_cert_pem.subject.to_a.each do |attribute|
40-
return Base64.strict_encode64(x5_cert_pem.to_der) if attribute[1].include?(key_alias)
41-
end
42-
unless p12_file.ca_certs.nil?
43-
p12_file.ca_certs.each do |cert|
36+
def self.getCertBasedOnKeyAlias(x5_certs, key_alias)
37+
unless x5_certs.nil?
38+
x5_certs.each do |cert|
4439
cert.subject.to_a.each do |attribute|
45-
return Base64.strict_encode64(cert.to_der) if attribute[1].include?(key_alias)
40+
return cert if attribute[1].include?(key_alias)
4641
end
4742
end
4843
end
4944
nil
5045
end
51-
end
46+
end
47+

lib/cybersource_rest_client/api_client.rb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
require 'tempfile'
1616
require 'typhoeus'
1717
require 'addressable/uri'
18+
require_relative 'ethon_extensions'
1819

1920
module CyberSource
2021
class ApiClient
@@ -148,7 +149,8 @@ def build_request(http_method, path, opts = {})
148149
:sslcert => @config.cert_file,
149150
:sslkeypasswd => @merchantconfig.sslKeyPassword || "",
150151
:sslkey => @config.key_file,
151-
:verbose => @config.debugging
152+
:verbose => @config.debugging,
153+
:maxage_conn => @merchantconfig.keepAliveTime || 118 # Default to 118 seconds as same as default of libcurl
152154
}
153155

154156
# set custom cert, if provided

0 commit comments

Comments
 (0)