acquiring-sdk-ruby 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/Gemfile +2 -0
- data/LICENSE.txt +22 -0
- data/README.md +140 -0
- data/Rakefile +34 -0
- data/acquiring-sdk-ruby.gemspec +30 -0
- data/lib/worldline/acquiring/sdk/api_resource.rb +53 -0
- data/lib/worldline/acquiring/sdk/authentication/authenticator.rb +21 -0
- data/lib/worldline/acquiring/sdk/authentication/authorization_type.rb +17 -0
- data/lib/worldline/acquiring/sdk/authentication/oauth2_authenticator.rb +142 -0
- data/lib/worldline/acquiring/sdk/authentication/oauth2_exception.rb +15 -0
- data/lib/worldline/acquiring/sdk/authentication.rb +1 -0
- data/lib/worldline/acquiring/sdk/call_context.rb +9 -0
- data/lib/worldline/acquiring/sdk/client.rb +69 -0
- data/lib/worldline/acquiring/sdk/communication/communication_exception.rb +21 -0
- data/lib/worldline/acquiring/sdk/communication/connection.rb +50 -0
- data/lib/worldline/acquiring/sdk/communication/default_connection.rb +429 -0
- data/lib/worldline/acquiring/sdk/communication/metadata_provider.rb +162 -0
- data/lib/worldline/acquiring/sdk/communication/multipart_form_data_object.rb +54 -0
- data/lib/worldline/acquiring/sdk/communication/multipart_form_data_request.rb +15 -0
- data/lib/worldline/acquiring/sdk/communication/not_found_exception.rb +21 -0
- data/lib/worldline/acquiring/sdk/communication/param_request.rb +16 -0
- data/lib/worldline/acquiring/sdk/communication/pooled_connection.rb +28 -0
- data/lib/worldline/acquiring/sdk/communication/request_header.rb +64 -0
- data/lib/worldline/acquiring/sdk/communication/request_param.rb +30 -0
- data/lib/worldline/acquiring/sdk/communication/response_exception.rb +58 -0
- data/lib/worldline/acquiring/sdk/communication/response_header.rb +80 -0
- data/lib/worldline/acquiring/sdk/communication.rb +1 -0
- data/lib/worldline/acquiring/sdk/communicator.rb +506 -0
- data/lib/worldline/acquiring/sdk/communicator_configuration.rb +197 -0
- data/lib/worldline/acquiring/sdk/domain/data_object.rb +34 -0
- data/lib/worldline/acquiring/sdk/domain/shopping_cart_extension.rb +62 -0
- data/lib/worldline/acquiring/sdk/domain/uploadable_file.rb +35 -0
- data/lib/worldline/acquiring/sdk/domain.rb +1 -0
- data/lib/worldline/acquiring/sdk/factory.rb +183 -0
- data/lib/worldline/acquiring/sdk/json/default_marshaller.rb +36 -0
- data/lib/worldline/acquiring/sdk/json/marshaller.rb +29 -0
- data/lib/worldline/acquiring/sdk/json/marshaller_syntax_exception.rb +11 -0
- data/lib/worldline/acquiring/sdk/json.rb +1 -0
- data/lib/worldline/acquiring/sdk/logging/communicator_logger.rb +26 -0
- data/lib/worldline/acquiring/sdk/logging/log_message_builder.rb +91 -0
- data/lib/worldline/acquiring/sdk/logging/logging_capable.rb +19 -0
- data/lib/worldline/acquiring/sdk/logging/obfuscation/body_obfuscator.rb +101 -0
- data/lib/worldline/acquiring/sdk/logging/obfuscation/header_obfuscator.rb +54 -0
- data/lib/worldline/acquiring/sdk/logging/obfuscation/obfuscation_capable.rb +23 -0
- data/lib/worldline/acquiring/sdk/logging/obfuscation/obfuscation_rule.rb +49 -0
- data/lib/worldline/acquiring/sdk/logging/obfuscation.rb +1 -0
- data/lib/worldline/acquiring/sdk/logging/request_log_message_builder.rb +52 -0
- data/lib/worldline/acquiring/sdk/logging/response_log_message_builder.rb +43 -0
- data/lib/worldline/acquiring/sdk/logging/ruby_communicator_logger.rb +63 -0
- data/lib/worldline/acquiring/sdk/logging/stdout_communicator_logger.rb +33 -0
- data/lib/worldline/acquiring/sdk/logging.rb +1 -0
- data/lib/worldline/acquiring/sdk/proxy_configuration.rb +76 -0
- data/lib/worldline/acquiring/sdk/v1/acquirer/acquirer_client.rb +35 -0
- data/lib/worldline/acquiring/sdk/v1/acquirer/merchant/accountverifications/account_verifications_client.rb +60 -0
- data/lib/worldline/acquiring/sdk/v1/acquirer/merchant/accountverifications.rb +4 -0
- data/lib/worldline/acquiring/sdk/v1/acquirer/merchant/dynamiccurrencyconversion/dynamic_currency_conversion_client.rb +60 -0
- data/lib/worldline/acquiring/sdk/v1/acquirer/merchant/dynamiccurrencyconversion.rb +4 -0
- data/lib/worldline/acquiring/sdk/v1/acquirer/merchant/merchant_client.rb +66 -0
- data/lib/worldline/acquiring/sdk/v1/acquirer/merchant/payments/get_payment_status_params.rb +34 -0
- data/lib/worldline/acquiring/sdk/v1/acquirer/merchant/payments/payments_client.rb +224 -0
- data/lib/worldline/acquiring/sdk/v1/acquirer/merchant/payments.rb +4 -0
- data/lib/worldline/acquiring/sdk/v1/acquirer/merchant/refunds/get_refund_params.rb +34 -0
- data/lib/worldline/acquiring/sdk/v1/acquirer/merchant/refunds/refunds_client.rb +157 -0
- data/lib/worldline/acquiring/sdk/v1/acquirer/merchant/refunds.rb +4 -0
- data/lib/worldline/acquiring/sdk/v1/acquirer/merchant/technicalreversals/technical_reversals_client.rb +64 -0
- data/lib/worldline/acquiring/sdk/v1/acquirer/merchant/technicalreversals.rb +4 -0
- data/lib/worldline/acquiring/sdk/v1/acquirer/merchant.rb +4 -0
- data/lib/worldline/acquiring/sdk/v1/acquirer.rb +4 -0
- data/lib/worldline/acquiring/sdk/v1/api_exception.rb +63 -0
- data/lib/worldline/acquiring/sdk/v1/authorization_exception.rb +23 -0
- data/lib/worldline/acquiring/sdk/v1/domain/address_verification_data.rb +41 -0
- data/lib/worldline/acquiring/sdk/v1/domain/amount_data.rb +48 -0
- data/lib/worldline/acquiring/sdk/v1/domain/api_account_verification_request.rb +70 -0
- data/lib/worldline/acquiring/sdk/v1/domain/api_account_verification_response.rb +87 -0
- data/lib/worldline/acquiring/sdk/v1/domain/api_action_response.rb +71 -0
- data/lib/worldline/acquiring/sdk/v1/domain/api_action_response_for_refund.rb +71 -0
- data/lib/worldline/acquiring/sdk/v1/domain/api_capture_request.rb +75 -0
- data/lib/worldline/acquiring/sdk/v1/domain/api_capture_request_for_refund.rb +43 -0
- data/lib/worldline/acquiring/sdk/v1/domain/api_increment_request.rb +61 -0
- data/lib/worldline/acquiring/sdk/v1/domain/api_increment_response.rb +43 -0
- data/lib/worldline/acquiring/sdk/v1/domain/api_payment_error_response.rb +62 -0
- data/lib/worldline/acquiring/sdk/v1/domain/api_payment_refund_request.rb +77 -0
- data/lib/worldline/acquiring/sdk/v1/domain/api_payment_request.rb +95 -0
- data/lib/worldline/acquiring/sdk/v1/domain/api_payment_resource.rb +103 -0
- data/lib/worldline/acquiring/sdk/v1/domain/api_payment_response.rb +126 -0
- data/lib/worldline/acquiring/sdk/v1/domain/api_payment_reversal_request.rb +61 -0
- data/lib/worldline/acquiring/sdk/v1/domain/api_payment_summary_for_response.rb +66 -0
- data/lib/worldline/acquiring/sdk/v1/domain/api_references_for_responses.rb +48 -0
- data/lib/worldline/acquiring/sdk/v1/domain/api_refund_request.rb +88 -0
- data/lib/worldline/acquiring/sdk/v1/domain/api_refund_resource.rb +110 -0
- data/lib/worldline/acquiring/sdk/v1/domain/api_refund_response.rb +133 -0
- data/lib/worldline/acquiring/sdk/v1/domain/api_refund_summary_for_response.rb +66 -0
- data/lib/worldline/acquiring/sdk/v1/domain/api_reversal_response.rb +36 -0
- data/lib/worldline/acquiring/sdk/v1/domain/api_technical_reversal_request.rb +50 -0
- data/lib/worldline/acquiring/sdk/v1/domain/api_technical_reversal_response.rb +62 -0
- data/lib/worldline/acquiring/sdk/v1/domain/card_data_for_dcc.rb +48 -0
- data/lib/worldline/acquiring/sdk/v1/domain/card_on_file_data.rb +52 -0
- data/lib/worldline/acquiring/sdk/v1/domain/card_payment_data.rb +114 -0
- data/lib/worldline/acquiring/sdk/v1/domain/card_payment_data_for_refund.rb +82 -0
- data/lib/worldline/acquiring/sdk/v1/domain/card_payment_data_for_resource.rb +43 -0
- data/lib/worldline/acquiring/sdk/v1/domain/card_payment_data_for_response.rb +52 -0
- data/lib/worldline/acquiring/sdk/v1/domain/card_payment_data_for_verification.rb +91 -0
- data/lib/worldline/acquiring/sdk/v1/domain/dcc_data.rb +55 -0
- data/lib/worldline/acquiring/sdk/v1/domain/dcc_proposal.rb +60 -0
- data/lib/worldline/acquiring/sdk/v1/domain/e_commerce_data.rb +52 -0
- data/lib/worldline/acquiring/sdk/v1/domain/e_commerce_data_for_account_verification.rb +45 -0
- data/lib/worldline/acquiring/sdk/v1/domain/e_commerce_data_for_response.rb +41 -0
- data/lib/worldline/acquiring/sdk/v1/domain/get_dcc_rate_request.rb +75 -0
- data/lib/worldline/acquiring/sdk/v1/domain/get_dcc_rate_response.rb +57 -0
- data/lib/worldline/acquiring/sdk/v1/domain/initial_card_on_file_data.rb +41 -0
- data/lib/worldline/acquiring/sdk/v1/domain/merchant_data.rb +76 -0
- data/lib/worldline/acquiring/sdk/v1/domain/network_token_data.rb +41 -0
- data/lib/worldline/acquiring/sdk/v1/domain/payment_references.rb +48 -0
- data/lib/worldline/acquiring/sdk/v1/domain/plain_card_data.rb +48 -0
- data/lib/worldline/acquiring/sdk/v1/domain/point_of_sale_data.rb +34 -0
- data/lib/worldline/acquiring/sdk/v1/domain/point_of_sale_data_for_dcc.rb +41 -0
- data/lib/worldline/acquiring/sdk/v1/domain/rate_data.rb +64 -0
- data/lib/worldline/acquiring/sdk/v1/domain/sub_operation.rb +94 -0
- data/lib/worldline/acquiring/sdk/v1/domain/sub_operation_for_refund.rb +87 -0
- data/lib/worldline/acquiring/sdk/v1/domain/subsequent_card_on_file_data.rb +48 -0
- data/lib/worldline/acquiring/sdk/v1/domain/three_d_secure.rb +62 -0
- data/lib/worldline/acquiring/sdk/v1/domain/transaction_data_for_dcc.rb +52 -0
- data/lib/worldline/acquiring/sdk/v1/domain.rb +4 -0
- data/lib/worldline/acquiring/sdk/v1/exception_factory.rb +48 -0
- data/lib/worldline/acquiring/sdk/v1/ping/ping_client.rb +52 -0
- data/lib/worldline/acquiring/sdk/v1/ping.rb +4 -0
- data/lib/worldline/acquiring/sdk/v1/platform_exception.rb +23 -0
- data/lib/worldline/acquiring/sdk/v1/reference_exception.rb +23 -0
- data/lib/worldline/acquiring/sdk/v1/v1_client.rb +43 -0
- data/lib/worldline/acquiring/sdk/v1/validation_exception.rb +23 -0
- data/lib/worldline/acquiring/sdk/v1.rb +4 -0
- data/lib/worldline/acquiring/sdk.rb +1 -0
- data/spec/comparable_extension.rb +29 -0
- data/spec/fixtures/resources/authentication/oauth2AccessToken.expired.json +4 -0
- data/spec/fixtures/resources/authentication/oauth2AccessToken.invalidClient.json +4 -0
- data/spec/fixtures/resources/authentication/oauth2AccessToken.json +4 -0
- data/spec/fixtures/resources/communication/getWithQueryParams.json +3 -0
- data/spec/fixtures/resources/communication/getWithoutQueryParams.json +3 -0
- data/spec/fixtures/resources/communication/notFound.html +1 -0
- data/spec/fixtures/resources/communication/postWithBadRequestResponse.json +11 -0
- data/spec/fixtures/resources/communication/postWithCreatedResponse.json +6 -0
- data/spec/fixtures/resources/communication/unknownServerError.json +10 -0
- data/spec/fixtures/resources/logging/bodyNoObfuscation.json +7 -0
- data/spec/fixtures/resources/logging/bodyWithBinObfuscated.json +3 -0
- data/spec/fixtures/resources/logging/bodyWithBinOriginal.json +3 -0
- data/spec/fixtures/resources/logging/bodyWithCardCustomObfuscated.json +13 -0
- data/spec/fixtures/resources/logging/bodyWithCardObfuscated.json +13 -0
- data/spec/fixtures/resources/logging/bodyWithCardOriginal.json +13 -0
- data/spec/fixtures/resources/logging/bodyWithObjectObfuscated.json +5 -0
- data/spec/fixtures/resources/logging/bodyWithObjectOriginal.json +5 -0
- data/spec/fixtures/resources/properties.oauth2.yml +8 -0
- data/spec/fixtures/resources/properties.proxy.yml +14 -0
- data/spec/integration/connection_pooling_spec.rb +74 -0
- data/spec/integration/multipart_form_data_spec.rb +216 -0
- data/spec/integration/process_payment_spec.rb +43 -0
- data/spec/integration/request_dcc_rate_spec.rb +24 -0
- data/spec/integration/sdk_proxy_spec.rb +70 -0
- data/spec/integration_setup.rb +111 -0
- data/spec/lib/authentication/oauth2_authenticator_spec.rb +68 -0
- data/spec/lib/client_spec.rb +47 -0
- data/spec/lib/communication/default_connection_logger_spec.rb +484 -0
- data/spec/lib/communication/default_connection_spec.rb +352 -0
- data/spec/lib/communication/metadata_provider_spec.rb +93 -0
- data/spec/lib/communicator_configuration_spec.rb +181 -0
- data/spec/lib/communicator_spec.rb +34 -0
- data/spec/lib/factory_spec.rb +38 -0
- data/spec/lib/json/default_marshaller_spec.rb +39 -0
- data/spec/lib/logging/obfuscation/body_obfuscator_spec.rb +86 -0
- data/spec/lib/logging/obfuscation/header_obfuscator_spec.rb +100 -0
- data/spec/lib/logging/ruby_communicator_logger_spec.rb +92 -0
- data/spec/lib/logging/stdout_communicator_logger_spec.rb +64 -0
- data/spec/spec_helper.rb +32 -0
- metadata +375 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
require 'worldline/acquiring/sdk/logging/obfuscation/body_obfuscator'
|
|
2
|
+
require 'worldline/acquiring/sdk/logging/obfuscation/header_obfuscator'
|
|
3
|
+
|
|
4
|
+
module Worldline
|
|
5
|
+
module Acquiring
|
|
6
|
+
module SDK
|
|
7
|
+
module Logging
|
|
8
|
+
# Abstract class used to construct a message describing a request or response.
|
|
9
|
+
#
|
|
10
|
+
# @attr_reader [String] request_id An identifier assigned to the request and response
|
|
11
|
+
# @attr_reader [String] headers Request or response headers in string form
|
|
12
|
+
# @attr_reader [String] body Request or response body as a string
|
|
13
|
+
# @attr_reader [String] content_type Content type of the body, generally 'application/json' or 'text/html'
|
|
14
|
+
# @attr_reader [Worldline::Acquiring::SDK::Logging::Obfuscation::BodyObfuscator] body_obfuscator
|
|
15
|
+
# @attr_reader [Worldline::Acquiring::SDK::Logging::Obfuscation::HeaderObfuscator] header_obfuscator
|
|
16
|
+
class LogMessageBuilder
|
|
17
|
+
|
|
18
|
+
attr_reader :request_id
|
|
19
|
+
attr_reader :headers
|
|
20
|
+
attr_reader :body
|
|
21
|
+
attr_reader :content_type
|
|
22
|
+
attr_reader :body_obfuscator
|
|
23
|
+
attr_reader :header_obfuscator
|
|
24
|
+
|
|
25
|
+
# Create a new LogMessageBuilder
|
|
26
|
+
def initialize(request_id,
|
|
27
|
+
body_obfuscator = Obfuscation::BodyObfuscator.default_obfuscator,
|
|
28
|
+
header_obfuscator = Obfuscation::HeaderObfuscator.default_obfuscator)
|
|
29
|
+
raise ArgumentError if request_id.nil? or request_id.empty?
|
|
30
|
+
raise ArgumentError if body_obfuscator.nil?
|
|
31
|
+
raise ArgumentError if header_obfuscator.nil?
|
|
32
|
+
@request_id = request_id
|
|
33
|
+
@headers = ''
|
|
34
|
+
@body_obfuscator = body_obfuscator
|
|
35
|
+
@header_obfuscator = header_obfuscator
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Adds a single header to the #headers string
|
|
39
|
+
def add_headers(name, value)
|
|
40
|
+
@headers += ', ' if @headers.length > 0
|
|
41
|
+
@headers += name + '="'
|
|
42
|
+
@headers += @header_obfuscator.obfuscate_header(name, value) unless value.nil?
|
|
43
|
+
@headers += '"'
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Sets the body of this message to the parameter body.
|
|
47
|
+
|
|
48
|
+
# @param body [String] the message body
|
|
49
|
+
# @param content_type [String] the content type of the body
|
|
50
|
+
def set_body(body, content_type)
|
|
51
|
+
if is_binary(content_type)
|
|
52
|
+
@body = "<binary content>"
|
|
53
|
+
else
|
|
54
|
+
@body = @body_obfuscator.obfuscate_body(body)
|
|
55
|
+
end
|
|
56
|
+
@content_type = content_type
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Constructs and returns the log message as a string.
|
|
60
|
+
# @return [String]
|
|
61
|
+
def get_message
|
|
62
|
+
raise NotImplementedError.new("#{self.class.name}#get_message() is not implemented.")
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def to_s
|
|
66
|
+
if self.class == LogMessageBuilder
|
|
67
|
+
super.to_s
|
|
68
|
+
else
|
|
69
|
+
get_message
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Returns an empty string if the parameter is nil, and returns the parameter itself otherwise
|
|
74
|
+
protected def empty_if_null(value)
|
|
75
|
+
value.nil? ? '' : value
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Returns whether or not the content type is binary
|
|
79
|
+
def is_binary(content_type)
|
|
80
|
+
if content_type.nil?
|
|
81
|
+
false
|
|
82
|
+
else
|
|
83
|
+
content_type = content_type.downcase
|
|
84
|
+
!(content_type.start_with?("text/") || content_type.include?("json") || content_type.include?("xml"))
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
module Worldline
|
|
2
|
+
module Acquiring
|
|
3
|
+
module SDK
|
|
4
|
+
module Logging
|
|
5
|
+
# Abstract mixin module that allows loggers to be registered to an object.
|
|
6
|
+
module LoggingCapable
|
|
7
|
+
|
|
8
|
+
def enable_logging(communicator_logger)
|
|
9
|
+
raise NotImplementedError
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def disable_logging
|
|
13
|
+
raise NotImplementedError
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
require 'worldline/acquiring/sdk/logging/obfuscation/obfuscation_rule'
|
|
2
|
+
|
|
3
|
+
module Worldline
|
|
4
|
+
module Acquiring
|
|
5
|
+
module SDK
|
|
6
|
+
module Logging
|
|
7
|
+
module Obfuscation
|
|
8
|
+
# A class that can be used to obfuscate properties in JSON bodies.
|
|
9
|
+
class BodyObfuscator
|
|
10
|
+
|
|
11
|
+
# Creates a new body obfuscator.
|
|
12
|
+
# This will contain some pre-defined obfuscation rules, as well as any provided custom rules
|
|
13
|
+
#
|
|
14
|
+
# @param additional_rules [Hash] An optional hash where the keys are property names and the values are
|
|
15
|
+
# functions that obfuscate a single value
|
|
16
|
+
def initialize(additional_rules = nil)
|
|
17
|
+
@obfuscation_rules = {
|
|
18
|
+
"address" => Obfuscation.obfuscate_all,
|
|
19
|
+
"authenticationValue" => Obfuscation.obfuscate_all_but_first(4),
|
|
20
|
+
"bin" => Obfuscation.obfuscate_all_but_first(6),
|
|
21
|
+
"cardholderAddress" => Obfuscation.obfuscate_all,
|
|
22
|
+
"cardholderPostalCode" => Obfuscation.obfuscate_all,
|
|
23
|
+
"cardNumber" => Obfuscation.obfuscate_all_but_last(4),
|
|
24
|
+
"cardSecurityCode" => Obfuscation.obfuscate_all,
|
|
25
|
+
"city" => Obfuscation.obfuscate_all,
|
|
26
|
+
"cryptogram" => Obfuscation.obfuscate_all_but_first(4),
|
|
27
|
+
"expiryDate" => Obfuscation.obfuscate_all_but_last(4),
|
|
28
|
+
"name" => Obfuscation.obfuscate_all,
|
|
29
|
+
"paymentAccountReference" => Obfuscation.obfuscate_all_but_first(6),
|
|
30
|
+
"postalCode" => Obfuscation.obfuscate_all,
|
|
31
|
+
"stateCode" => Obfuscation.obfuscate_all,
|
|
32
|
+
}
|
|
33
|
+
if additional_rules
|
|
34
|
+
additional_rules.each do |name, rule|
|
|
35
|
+
@obfuscation_rules[name] = rule
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
@property_pattern = build_property_pattern(@obfuscation_rules.keys)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def build_property_pattern(pn)
|
|
45
|
+
return /$^/ if pn.empty? # no possible match
|
|
46
|
+
# Regex to create:
|
|
47
|
+
# (["'])(X|Y|Z)\1\s*:\s*(?:(["'])(.*?)(?<!\\)\3|([^"'\s\[\{]\S*))
|
|
48
|
+
# Groups:
|
|
49
|
+
# 1: opening " or ' for the property name
|
|
50
|
+
# 2: property name
|
|
51
|
+
# 3: opening " or ' for the value
|
|
52
|
+
# 4: quoted value
|
|
53
|
+
# 5: non-quoted-value
|
|
54
|
+
# The negative lookbehind is to allow escaped quotes to be part of
|
|
55
|
+
# the value. What this does not allow currently is having values end
|
|
56
|
+
# with a \ (which would be escaped to \\).
|
|
57
|
+
regex = pn.inject("([\"'])(") { |r, p| "#{r}#{Regexp.quote(p)}|" }.chop <<
|
|
58
|
+
")\\1\\s*:\\s*(?:([\"'])(.*?)(?<!\\\\)\\3|([^\"'\\s\\[\\{]((?!,)\\S)*))"
|
|
59
|
+
/#{regex}/m # dotall mode
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def obfuscate_value(property_name, value)
|
|
63
|
+
obfuscation_rule = @obfuscation_rules[property_name]
|
|
64
|
+
return obfuscation_rule.call(value) if obfuscation_rule
|
|
65
|
+
value
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
public
|
|
69
|
+
|
|
70
|
+
# Obfuscates the given body as necessary.
|
|
71
|
+
#
|
|
72
|
+
# @return (String)
|
|
73
|
+
def obfuscate_body(body)
|
|
74
|
+
return nil if body.nil?
|
|
75
|
+
return '' if body.empty?
|
|
76
|
+
|
|
77
|
+
body.gsub(@property_pattern) do
|
|
78
|
+
m = Regexp.last_match
|
|
79
|
+
property_name = m[2]
|
|
80
|
+
value = m[4] || m[5]
|
|
81
|
+
# copy value 'cause it's part of m[0]
|
|
82
|
+
m[0].sub(value, obfuscate_value(property_name, value.dup))
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
private
|
|
87
|
+
|
|
88
|
+
DEFAULT_OBFUSCATOR = BodyObfuscator.new
|
|
89
|
+
|
|
90
|
+
public
|
|
91
|
+
|
|
92
|
+
# @return [BodyObfuscator]
|
|
93
|
+
def self.default_obfuscator
|
|
94
|
+
DEFAULT_OBFUSCATOR
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
require 'worldline/acquiring/sdk/logging/obfuscation/obfuscation_rule'
|
|
2
|
+
|
|
3
|
+
module Worldline
|
|
4
|
+
module Acquiring
|
|
5
|
+
module SDK
|
|
6
|
+
module Logging
|
|
7
|
+
module Obfuscation
|
|
8
|
+
# A class that can be used to obfuscate headers.
|
|
9
|
+
class HeaderObfuscator
|
|
10
|
+
|
|
11
|
+
# Creates a new header obfuscator.
|
|
12
|
+
# This will contain some pre-defined obfuscation rules, as well as any provided custom rules
|
|
13
|
+
#
|
|
14
|
+
# @param additional_rules [Hash] An optional hash where the keys are header names and the values are
|
|
15
|
+
# functions that obfuscate a single value
|
|
16
|
+
def initialize(additional_rules = nil)
|
|
17
|
+
@obfuscation_rules = {
|
|
18
|
+
"authorization" => Obfuscation.obfuscate_with_fixed_length(8),
|
|
19
|
+
"www-authenticate" => Obfuscation.obfuscate_with_fixed_length(8),
|
|
20
|
+
"proxy-authenticate" => Obfuscation.obfuscate_with_fixed_length(8),
|
|
21
|
+
"proxy-authorization" => Obfuscation.obfuscate_with_fixed_length(8),
|
|
22
|
+
}
|
|
23
|
+
if additional_rules
|
|
24
|
+
additional_rules.each do |name, rule|
|
|
25
|
+
@obfuscation_rules[name.downcase] = rule
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Obfuscates the value for the given header as necessary.
|
|
31
|
+
#
|
|
32
|
+
# @return (String)
|
|
33
|
+
def obfuscate_header(header_name, value)
|
|
34
|
+
obfuscation_rule = @obfuscation_rules[header_name.downcase]
|
|
35
|
+
return obfuscation_rule.call(value) if obfuscation_rule
|
|
36
|
+
value
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
DEFAULT_OBFUSCATOR = HeaderObfuscator.new
|
|
42
|
+
|
|
43
|
+
public
|
|
44
|
+
|
|
45
|
+
# @return [HeaderObfuscator]
|
|
46
|
+
def self.default_obfuscator
|
|
47
|
+
DEFAULT_OBFUSCATOR
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
module Worldline
|
|
2
|
+
module Acquiring
|
|
3
|
+
module SDK
|
|
4
|
+
module Logging
|
|
5
|
+
module Obfuscation
|
|
6
|
+
# Abstract mixin module that allows specifying body and header obfuscators for an object.
|
|
7
|
+
module ObfuscationCapable
|
|
8
|
+
|
|
9
|
+
# Sets the current body obfuscator to use.
|
|
10
|
+
def set_body_obfuscator(body_obfuscator)
|
|
11
|
+
raise NotImplementedError
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Sets the current header obfuscator to use.
|
|
15
|
+
def set_header_obfuscator(header_obfuscator)
|
|
16
|
+
raise NotImplementedError
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
module Worldline
|
|
2
|
+
module Acquiring
|
|
3
|
+
module SDK
|
|
4
|
+
module Logging
|
|
5
|
+
module Obfuscation
|
|
6
|
+
|
|
7
|
+
# Returns an obfuscation rule (callable) that will replace all characters with *
|
|
8
|
+
def self.obfuscate_all
|
|
9
|
+
->(value) do
|
|
10
|
+
return value if value.nil? or value.empty?
|
|
11
|
+
'*' * (value || '').length
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Returns an obfuscation rule (function) that will replace values with a fixed length string containing only *
|
|
16
|
+
def self.obfuscate_with_fixed_length(fixed_length)
|
|
17
|
+
->(value) { '*' * fixed_length }
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Returns an obfuscation rule (function) that will keep a fixed number of characters at the start,
|
|
21
|
+
# then replaces all other characters with *
|
|
22
|
+
def self.obfuscate_all_but_first(count)
|
|
23
|
+
->(value) do
|
|
24
|
+
return value if value.nil? or value.empty?
|
|
25
|
+
return value if value.length < count
|
|
26
|
+
# range describes the range of characters to replace with asterisks
|
|
27
|
+
range = count...value.length
|
|
28
|
+
value[range] = '*' * range.size
|
|
29
|
+
value
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Returns an obfuscation rule that will keep a fixed number of characters at the end,
|
|
34
|
+
# then replaces all other characters with *
|
|
35
|
+
def self.obfuscate_all_but_last(count)
|
|
36
|
+
->(value) do
|
|
37
|
+
return value if value.nil? or value.empty?
|
|
38
|
+
return value if value.length < count
|
|
39
|
+
# range describes the range of characters to replace with asterisks
|
|
40
|
+
range = 0...(value.length - count)
|
|
41
|
+
value[range] = '*' * range.size
|
|
42
|
+
value
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Dir[File.join(__dir__, 'obfuscation', '*.rb')].each { |f| require f }
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
require 'worldline/acquiring/sdk/logging/obfuscation/body_obfuscator'
|
|
2
|
+
require 'worldline/acquiring/sdk/logging/obfuscation/header_obfuscator'
|
|
3
|
+
require 'worldline/acquiring/sdk/logging/log_message_builder'
|
|
4
|
+
|
|
5
|
+
module Worldline
|
|
6
|
+
module Acquiring
|
|
7
|
+
module SDK
|
|
8
|
+
module Logging
|
|
9
|
+
# Class that converts data about a request into a properly formatted log message.
|
|
10
|
+
# Formats request id, http method, uri, headers and body into a helpful message.
|
|
11
|
+
class RequestLogMessageBuilder < LogMessageBuilder
|
|
12
|
+
|
|
13
|
+
def initialize(request_id, method, uri,
|
|
14
|
+
body_obfuscator = Obfuscation::BodyObfuscator.default_obfuscator,
|
|
15
|
+
header_obfuscator = Obfuscation::HeaderObfuscator.default_obfuscator)
|
|
16
|
+
super(request_id, body_obfuscator, header_obfuscator)
|
|
17
|
+
@method = method
|
|
18
|
+
@uri = uri
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Constructs and returns a log message based on the request data. The log message is a string.
|
|
22
|
+
def get_message
|
|
23
|
+
msg_template_without_body = "Outgoing request (requestId='%s'):\n" +
|
|
24
|
+
" method: '%s'\n" +
|
|
25
|
+
" uri: '%s'\n" +
|
|
26
|
+
" headers: '%s'"
|
|
27
|
+
msg_template_with_body = msg_template_without_body + "\n" +
|
|
28
|
+
" content-type: '%s'\n" +
|
|
29
|
+
" body: '%s'"
|
|
30
|
+
|
|
31
|
+
return sprintf(msg_template_without_body, @request_id, empty_if_null(@method),
|
|
32
|
+
format_uri, @headers) if @body.nil?
|
|
33
|
+
sprintf(msg_template_with_body, @request_id, empty_if_null(@method),
|
|
34
|
+
format_uri, @headers, empty_if_null(@content_type), @body)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def format_uri
|
|
40
|
+
'' unless @uri && @uri.path
|
|
41
|
+
if @uri.query.nil?
|
|
42
|
+
@uri.path
|
|
43
|
+
else
|
|
44
|
+
"#{@uri.path}?#{@uri.query}" unless @uri.query.nil?
|
|
45
|
+
end
|
|
46
|
+
# @uri.path + '?' + empty_if_null(@uri.query)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
require 'worldline/acquiring/sdk/logging/obfuscation/body_obfuscator'
|
|
2
|
+
require 'worldline/acquiring/sdk/logging/obfuscation/header_obfuscator'
|
|
3
|
+
require 'worldline/acquiring/sdk/logging/log_message_builder'
|
|
4
|
+
|
|
5
|
+
module Worldline
|
|
6
|
+
module Acquiring
|
|
7
|
+
module SDK
|
|
8
|
+
module Logging
|
|
9
|
+
# Class that converts data about a response into a properly formatted log message.
|
|
10
|
+
# Formats request id, status code, headers, body and time between request and response into a helpful message.
|
|
11
|
+
class ResponseLogMessageBuilder < LogMessageBuilder
|
|
12
|
+
|
|
13
|
+
# @param request_id [String] identifier of the request corresponding to this response.
|
|
14
|
+
# @param status_code [Integer] HTTP status code of the response.
|
|
15
|
+
# @param duration [Float] time elapsed between request and response.
|
|
16
|
+
def initialize(request_id, status_code, duration = -1,
|
|
17
|
+
body_obfuscator = Obfuscation::BodyObfuscator.default_obfuscator,
|
|
18
|
+
header_obfuscator = Obfuscation::HeaderObfuscator.default_obfuscator)
|
|
19
|
+
super(request_id, body_obfuscator, header_obfuscator)
|
|
20
|
+
@status_code = status_code
|
|
21
|
+
@duration = duration
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Constructs and returns a log message based on the request data. The log message is a string.
|
|
25
|
+
def get_message
|
|
26
|
+
msg_template = "Incoming response (requestId='%s'" +
|
|
27
|
+
((@duration < 0) ? "" : ", %.3f ms") +
|
|
28
|
+
"):\n" +
|
|
29
|
+
" status-code: '%s'\n" +
|
|
30
|
+
" headers: '%s'\n" +
|
|
31
|
+
" content-type: '%s'\n" +
|
|
32
|
+
" body: '%s'"
|
|
33
|
+
|
|
34
|
+
return sprintf(msg_template, @request_id, @status_code, @headers,
|
|
35
|
+
empty_if_null(@content_type), empty_if_null(@body)) if @duration < 0
|
|
36
|
+
sprintf(msg_template, @request_id, @duration, @status_code, @headers,
|
|
37
|
+
empty_if_null(@content_type), empty_if_null(@body))
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
require 'English'
|
|
2
|
+
require 'worldline/acquiring/sdk/logging/communicator_logger'
|
|
3
|
+
|
|
4
|
+
module Worldline
|
|
5
|
+
module Acquiring
|
|
6
|
+
module SDK
|
|
7
|
+
module Logging
|
|
8
|
+
# Logging class that Logs messages and errors to a logger.
|
|
9
|
+
# Errors can be logged at a separate level compared to regular messages.
|
|
10
|
+
class RubyCommunicatorLogger < CommunicatorLogger
|
|
11
|
+
|
|
12
|
+
# Creates a new RubyCommunicatorLogger instance.
|
|
13
|
+
#
|
|
14
|
+
# @param logger [Logger] the logger to log messages to. Messages to log will be provided using logger#log(message level, message)
|
|
15
|
+
# @param log_level [String] log level to use for non-error messages.
|
|
16
|
+
# @param error_level [String, nil] error logging level to use.
|
|
17
|
+
def initialize(logger, log_level, error_level = nil)
|
|
18
|
+
# implement the interface
|
|
19
|
+
error_level ||= log_level
|
|
20
|
+
raise ArgumentError unless logger
|
|
21
|
+
raise ArgumentError unless log_level
|
|
22
|
+
raise ArgumentError unless error_level
|
|
23
|
+
|
|
24
|
+
@logger = logger
|
|
25
|
+
@log_level = log_level
|
|
26
|
+
@error_level = error_level
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Logs a single error or non-error message to the logger.
|
|
30
|
+
def log(msg, thrown = nil)
|
|
31
|
+
# use Ruby Logger
|
|
32
|
+
if thrown
|
|
33
|
+
@logger.log(@error_level) { msg + $RS + thrown.to_s + $RS + thrown.backtrace.join($RS) }
|
|
34
|
+
else
|
|
35
|
+
@logger.log(@log_level, msg)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Opens or creates a new file in write-only mode with _filename_.
|
|
40
|
+
def self.create_logfile(filename)
|
|
41
|
+
logdev = begin
|
|
42
|
+
open(filename, (File::WRONLY | File::APPEND | File::CREAT | File::EXCL))
|
|
43
|
+
rescue Errno::EEXIST
|
|
44
|
+
# file is created by another process
|
|
45
|
+
open_logfile(filename)
|
|
46
|
+
end
|
|
47
|
+
logdev.sync = true
|
|
48
|
+
logdev
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Opens or creates a new file in write-only mode with _filename_.
|
|
52
|
+
def self.open_logfile(filename)
|
|
53
|
+
begin
|
|
54
|
+
open(filename, (File::WRONLY | File::APPEND))
|
|
55
|
+
rescue Errno::ENOENT
|
|
56
|
+
create_logfile(filename)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
require 'English'
|
|
2
|
+
require 'singleton'
|
|
3
|
+
require 'worldline/acquiring/sdk/logging/communicator_logger'
|
|
4
|
+
|
|
5
|
+
module Worldline
|
|
6
|
+
module Acquiring
|
|
7
|
+
module SDK
|
|
8
|
+
module Logging
|
|
9
|
+
# Logging class that logs the messages to $stdout.
|
|
10
|
+
class StdoutCommunicatorLogger < CommunicatorLogger
|
|
11
|
+
include Singleton
|
|
12
|
+
|
|
13
|
+
def initialize
|
|
14
|
+
# implement the interface
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Logs a single error or non-error message to $stdout.
|
|
18
|
+
def log(msg, thrown = nil)
|
|
19
|
+
$stdout.puts get_date_prefix + msg
|
|
20
|
+
$stdout.puts thrown.to_s if thrown
|
|
21
|
+
$stdout.puts thrown.backtrace.join($RS) if thrown
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def get_date_prefix
|
|
27
|
+
Time.now.strftime("%Y-%m-%dT%H:%M:%S ")
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Dir[File.join(__dir__, 'logging', '*.rb')].each { |f| require f }
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
module Worldline
|
|
2
|
+
module Acquiring
|
|
3
|
+
module SDK
|
|
4
|
+
# Contains the URL, username and password of a proxy.
|
|
5
|
+
#
|
|
6
|
+
# @attr [String] scheme Proxy scheme (http or https)
|
|
7
|
+
# @attr [String] host Proxy hostname
|
|
8
|
+
# @attr [Integer] port Proxy port
|
|
9
|
+
# @attr [String] username Proxy authentication username
|
|
10
|
+
# @attr [String] password Proxy authentication password
|
|
11
|
+
class ProxyConfiguration
|
|
12
|
+
|
|
13
|
+
# Initialize a new ProxyConfiguration from the parameter hash.
|
|
14
|
+
# In order to be complete either host, port and scheme, or an address is required.
|
|
15
|
+
#
|
|
16
|
+
# @param args [Hash] the parameters to initialize the proxy configuration with
|
|
17
|
+
# @option args [String] :host host part of the URL to the proxy.
|
|
18
|
+
# @option args [Integer] :port port the proxy will be accessed through.
|
|
19
|
+
# @option args [String] :scheme HTTP scheme used to communicate with the proxy (http or https).
|
|
20
|
+
# @option args [String] :address full URI to the proxy excluding username and password.
|
|
21
|
+
# If given this uri takes precedence over individual host, port and scheme.
|
|
22
|
+
# @option args [String] :username username used in authentication to the proxy.
|
|
23
|
+
# @option args [String] :password password used to authenticate to the proxy.
|
|
24
|
+
def initialize(args)
|
|
25
|
+
host = args[:host]
|
|
26
|
+
port = args[:port]
|
|
27
|
+
username = args[:username]
|
|
28
|
+
password = args[:password]
|
|
29
|
+
scheme = args[:scheme] || 'http'
|
|
30
|
+
|
|
31
|
+
# Don't switch the order, a given address overrides host, port and username
|
|
32
|
+
address = args[:address]
|
|
33
|
+
host = address.host if address
|
|
34
|
+
port = address.port if address
|
|
35
|
+
scheme = address.scheme if address
|
|
36
|
+
|
|
37
|
+
raise ArgumentError.new('scheme is required') unless scheme and not scheme.strip.empty?
|
|
38
|
+
raise ArgumentError.new('host is required') unless host and not host.strip.empty?
|
|
39
|
+
raise ArgumentError.new('port is required') unless port and port > 0 and port <= 65535
|
|
40
|
+
|
|
41
|
+
@host = host
|
|
42
|
+
@port = port
|
|
43
|
+
@username = username
|
|
44
|
+
@password = password
|
|
45
|
+
@scheme = scheme
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
attr_accessor :scheme
|
|
49
|
+
attr_accessor :host
|
|
50
|
+
attr_accessor :port
|
|
51
|
+
|
|
52
|
+
attr_accessor :username
|
|
53
|
+
attr_accessor :password
|
|
54
|
+
|
|
55
|
+
# @return [String] a URL string representation of the proxy, excluding authentication.
|
|
56
|
+
def proxy_uri
|
|
57
|
+
"#{scheme}://#{host}:#{port}"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def to_s
|
|
61
|
+
proxy_uri
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
def self.get_port(address)
|
|
67
|
+
port = address.port
|
|
68
|
+
return port if port != -1
|
|
69
|
+
return 80 if address.scheme.casecmp('http') == 0
|
|
70
|
+
return 443 if address.scheme.casecmp('https') == 0
|
|
71
|
+
raise ArgumentError.new('unsupported scheme: ' + address.scheme)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
#
|
|
2
|
+
# This file was automatically generated.
|
|
3
|
+
#
|
|
4
|
+
require 'worldline/acquiring/sdk/api_resource'
|
|
5
|
+
require 'worldline/acquiring/sdk/v1/acquirer/merchant/merchant_client'
|
|
6
|
+
|
|
7
|
+
module Worldline
|
|
8
|
+
module Acquiring
|
|
9
|
+
module SDK
|
|
10
|
+
module V1
|
|
11
|
+
module Acquirer
|
|
12
|
+
# Acquirer client. Thread-safe.
|
|
13
|
+
class AcquirerClient < Worldline::Acquiring::SDK::ApiResource
|
|
14
|
+
|
|
15
|
+
# @param parent [Worldline::Acquiring::SDK::ApiResource]
|
|
16
|
+
# @param path_context [Hash, nil]
|
|
17
|
+
def initialize(parent, path_context)
|
|
18
|
+
super(parent: parent, path_context: path_context)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Resource /processing/v1/{acquirerId}/{merchantId}
|
|
22
|
+
#
|
|
23
|
+
# @param merchant_id [String]
|
|
24
|
+
# @return [Worldline::Acquiring::SDK::V1::Acquirer::Merchant::MerchantClient]
|
|
25
|
+
def merchant(merchant_id)
|
|
26
|
+
Worldline::Acquiring::SDK::V1::Acquirer::Merchant::MerchantClient.new(self, {
|
|
27
|
+
'merchantId'.freeze => merchant_id,
|
|
28
|
+
})
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|