Skip to content

Commit

Permalink
Merge pull request #30 from alexdean/quote-identifiers
Browse files Browse the repository at this point in the history
quote AS2-To / AS2-From identifiers which contain spaces
  • Loading branch information
alexdean committed Aug 25, 2023
2 parents 95f9455 + f6144a9 commit d15814c
Show file tree
Hide file tree
Showing 10 changed files with 164 additions and 13 deletions.
9 changes: 9 additions & 0 deletions lib/as2.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,13 @@ def self.choose_mic_algorithm(disposition_notification_options)
parsed = As2::Parser::DispositionNotificationOptions.parse(disposition_notification_options)
Array(parsed['signed-receipt-micalg']).find { |m| As2::DigestSelector.valid?(m) }
end

# surround an As2-From/As2-To value with double-quotes, if it contains a space.
def self.quoted_system_identifier(name)
if name.to_s.include?(' ')
"\"#{name}\""
else
name
end
end
end
16 changes: 12 additions & 4 deletions lib/as2/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,8 @@ def send_file(file_name, content: nil, content_type: 'application/EDI-Consent')

req = Net::HTTP::Post.new @partner.url.path
req['AS2-Version'] = '1.0' # 1.1 includes compression support, which we dont implement.
req['AS2-From'] = as2_from
req['AS2-To'] = as2_to
req['AS2-From'] = As2.quoted_system_identifier(as2_from)
req['AS2-To'] = As2.quoted_system_identifier(as2_to)
req['Subject'] = 'AS2 Transaction'
req['Content-Type'] = 'application/pkcs7-mime; smime-type=enveloped-data; name=smime.p7m'
req['Date'] = Time.now.rfc2822
Expand Down Expand Up @@ -99,9 +99,16 @@ def send_file(file_name, content: nil, content_type: 'application/EDI-Consent')
# note: to pass this traffic through a debugging proxy (like Charles)
# set ENV['http_proxy'].
http = Net::HTTP.new(@partner.url.host, @partner.url.port)
http.use_ssl = @partner.url.scheme == 'https'

use_ssl = @partner.url.scheme == 'https'
http.use_ssl = use_ssl
if use_ssl
if @partner.tls_verify_mode
http.verify_mode = @partner.tls_verify_mode
end
end

# http.set_debug_output $stderr
# http.verify_mode = OpenSSL::SSL::VERIFY_NONE

http.start do
resp = http.request(req)
Expand All @@ -120,6 +127,7 @@ def send_file(file_name, content: nil, content_type: 'application/EDI-Consent')
end

Result.new(
request: req,
response: resp,
mic_matched: mdn_report[:mic_matched],
mid_matched: mdn_report[:mid_matched],
Expand Down
5 changes: 3 additions & 2 deletions lib/as2/client/result.rb
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
module As2
class Client
class Result
attr_reader :response, :mic_matched, :mid_matched, :body, :disposition, :signature_verification_error, :exception, :outbound_message_id
attr_reader :request, :response, :mic_matched, :mid_matched, :body, :disposition, :signature_verification_error, :exception, :outbound_message_id

def initialize(response:, mic_matched:, mid_matched:, body:, disposition:, signature_verification_error:, exception:, outbound_message_id:)
def initialize(request:, response:, mic_matched:, mid_matched:, body:, disposition:, signature_verification_error:, exception:, outbound_message_id:)
@request = request
@response = response
@mic_matched = mic_matched
@mid_matched = mid_matched
Expand Down
13 changes: 12 additions & 1 deletion lib/as2/config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ def self.build_certificate(input)
end
end

class Partner < Struct.new :name, :url, :certificate, :mdn_format, :outbound_format
class Partner < Struct.new :name, :url, :certificate, :tls_verify_mode, :mdn_format, :outbound_format
def url=(url)
if url.kind_of? String
self['url'] = URI.parse url
Expand Down Expand Up @@ -42,6 +42,17 @@ def outbound_format=(format)
def certificate=(certificate)
self['certificate'] = As2::Config.build_certificate(certificate)
end

# if set, will be used for SSL transmissions.
# @see `verify_mode` in https://ruby-doc.org/stdlib-2.7.1/libdoc/net/http/rdoc/Net/HTTP.html
def tls_verify_mode=(mode)
valid_modes = [nil, OpenSSL::SSL::VERIFY_NONE, OpenSSL::SSL::VERIFY_PEER]
if !valid_modes.include?(mode)
raise ArgumentError, "tls_verify_mode '#{mode}' must be one of #{valid_modes.inspect}"
end

self['tls_verify_mode'] = mode
end
end

class ServerInfo < Struct.new :name, :url, :certificate, :pkey, :domain
Expand Down
12 changes: 6 additions & 6 deletions lib/as2/server.rb
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,8 @@ def send_mdn(env, mic, mic_algorithm, failed = nil)

options = {
'Reporting-UA' => @server_info.name,
'Original-Recipient' => "rfc822; #{@server_info.name}",
'Final-Recipient' => "rfc822; #{@server_info.name}",
'Original-Recipient' => "rfc822; #{As2.quoted_system_identifier(@server_info.name)}",
'Final-Recipient' => "rfc822; #{As2.quoted_system_identifier(@server_info.name)}",
'Original-Message-ID' => env['HTTP_MESSAGE_ID']
}
if failed
Expand Down Expand Up @@ -148,8 +148,8 @@ def format_mdn_v0(mdn_text, as2_to:)
# TODO: if MIME-Version header is actually needed, should extract it out of smime_signed.
headers['MIME-Version'] = '1.0'
headers['Message-ID'] = As2.generate_message_id(@server_info)
headers['AS2-From'] = @server_info.name
headers['AS2-To'] = as2_to
headers['AS2-From'] = As2.quoted_system_identifier(@server_info.name)
headers['AS2-To'] = As2.quoted_system_identifier(as2_to)
headers['AS2-Version'] = '1.0'
headers['Connection'] = 'close'

Expand Down Expand Up @@ -191,8 +191,8 @@ def format_mdn_v1(mdn_text, as2_to:)
headers['Content-Type'] = "multipart/signed; protocol=\"application/pkcs7-signature\"; micalg=\"#{micalg}\"; boundary=\"#{boundary}\""
headers['MIME-Version'] = '1.0'
headers['Message-ID'] = As2.generate_message_id(@server_info)
headers['AS2-From'] = @server_info.name
headers['AS2-To'] = as2_to
headers['AS2-From'] = As2.quoted_system_identifier(@server_info.name)
headers['AS2-To'] = As2.quoted_system_identifier(as2_to)
headers['AS2-Version'] = '1.0'
headers['Connection'] = 'close'

Expand Down
18 changes: 18 additions & 0 deletions test/as2_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,22 @@
assert_equal 'md5', As2.choose_mic_algorithm(header_value)
end
end

describe '.quoted_system_identifier' do
it 'returns the string unchanged if it does not contain a space' do
assert_equal 'A', As2.quoted_system_identifier('A')
end

it 'surrounds name with double-quotes if it contains a space' do
assert_equal '"A A"', As2.quoted_system_identifier('A A')
end

it 'returns non-string inputs unchanged' do
assert_nil As2.quoted_system_identifier(nil)
assert_equal 1, As2.quoted_system_identifier(1)
assert_equal true, As2.quoted_system_identifier(true)
assert_equal :symbol, As2.quoted_system_identifier(:symbol)
assert_equal({}, As2.quoted_system_identifier({}))
end
end
end
1 change: 1 addition & 0 deletions test/client/result_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
signature_verification_error: nil,
mic_matched: true,
mid_matched: true,
request: nil,
response: nil,
body: nil,
exception: nil,
Expand Down
34 changes: 34 additions & 0 deletions test/client_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,40 @@ def setup_integration_scenario(
end

describe '#send_file' do
# these may contain spaces, which must be quoted.
# https://datatracker.ietf.org/doc/html/rfc4130#section-6.2
it "quotes As2-From and As2-To headers when they contain spaces" do
alice_partner = build_partner('A L I C E', credentials: 'client')
alice_server_info = build_server_info('A L I C E', credentials: 'client')
bob_partner = build_partner('B O B', credentials: 'server')
bob_server_info = build_server_info('B O B', credentials: 'server')

alice_client = As2::Client.new(bob_partner, server_info: alice_server_info)

WebMock.stub_request(:post, bob_partner.url).to_return do |request|
assert_equal '"A L I C E"', request.headers['As2-From']
assert_equal '"B O B"', request.headers['As2-To']
end

alice_client.send_file('data.txt', content: File.read('test/fixtures/message.txt'))
end

it "does not quotes As2-From and As2-To headers when they contain no spaces" do
alice_partner = build_partner('ALICE', credentials: 'client')
alice_server_info = build_server_info('ALICE', credentials: 'client')
bob_partner = build_partner('BOB', credentials: 'server')
bob_server_info = build_server_info('BOB', credentials: 'server')

alice_client = As2::Client.new(bob_partner, server_info: alice_server_info)

WebMock.stub_request(:post, bob_partner.url).to_return do |request|
assert_equal 'ALICE', request.headers['As2-From']
assert_equal 'BOB', request.headers['As2-To']
end

alice_client.send_file('data.txt', content: File.read('test/fixtures/message.txt'))
end

it 'considers a 2xx response code to be successful' do
setup_integration_scenario(http_response_status: '202')

Expand Down
21 changes: 21 additions & 0 deletions test/config_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,27 @@
assert_equal "outbound_format 'invalid' must be one of [\"v0\", \"v1\"]", error.message
end
end

describe '#tls_verify_mode=' do
it 'accepts an OpenSSL::SSL::VERIFY_* constant' do
@partner_config.tls_verify_mode = OpenSSL::SSL::VERIFY_PEER
assert_equal OpenSSL::SSL::VERIFY_PEER, @partner_config.tls_verify_mode

@partner_config.tls_verify_mode = OpenSSL::SSL::VERIFY_NONE
assert_equal OpenSSL::SSL::VERIFY_NONE, @partner_config.tls_verify_mode
end

it 'accepts nil' do
@partner_config.tls_verify_mode = nil
assert_nil @partner_config.tls_verify_mode
end

it 'raises if given an invalid value' do
assert_raises(ArgumentError) do
@partner_config.tls_verify_mode = 'invalid'
end
end
end
end

describe 'ServerInfo' do
Expand Down
48 changes: 48 additions & 0 deletions test/server_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,22 @@
assert_equal('UNKNOWN', headers['AS2-To'])
end

it 'quotes receipient fields if they contain spaces' do
partner = build_partner('A L I C E', credentials: 'client')
server_info = build_server_info('B O B', credentials: 'server')
server = As2::Server.new(server_info: server_info, partner: partner)

env = {
'HTTP_MESSAGE_ID' => '<message@server>',
'HTTP_AS2_FROM' => 'A L I C E'
}
_status, headers, body = server.send_mdn(env, 'micmicmic', 'sha256')

payload = body.first.strip
assert payload.include?('Original-Recipient: rfc822; "B O B"')
assert payload.include?('Final-Recipient: rfc822; "B O B"')
end

describe 'with mdn_format:v0' do
before do
@partner.mdn_format = 'v0'
Expand Down Expand Up @@ -127,6 +143,22 @@
assert_equal expected_plain_text.strip, plain_text.body.to_s.strip
assert_equal expected_notification.strip, notification.body.to_s.strip
end

it "quotes AS2-From/AS2-To identifiers if the contain spaces" do
partner = build_partner('A L I C E', credentials: 'client')
partner.mdn_format = 'v0'
server_info = build_server_info('B O B', credentials: 'server')
server = As2::Server.new(server_info: server_info, partner: partner)

env = {
'HTTP_MESSAGE_ID' => '<message@server>',
'HTTP_AS2_FROM' => 'A L I C E'
}
_status, headers, body = server.send_mdn(env, 'micmicmic', 'sha256')

assert_equal '"A L I C E"', headers['AS2-To']
assert_equal '"B O B"', headers['AS2-From']
end
end

describe 'with mdn_format:v1' do
Expand Down Expand Up @@ -226,6 +258,22 @@
assert_equal expected_plain_text.strip, plain_text.body.to_s.strip
assert_equal expected_notification.strip, notification.body.to_s.strip
end

it "quotes AS2-From/AS2-To identifiers if the contain spaces" do
partner = build_partner('A L I C E', credentials: 'client')
partner.mdn_format = 'v1'
server_info = build_server_info('B O B', credentials: 'server')
server = As2::Server.new(server_info: server_info, partner: partner)

env = {
'HTTP_MESSAGE_ID' => '<message@server>',
'HTTP_AS2_FROM' => 'A L I C E'
}
_status, headers, body = server.send_mdn(env, 'micmicmic', 'sha256')

assert_equal '"A L I C E"', headers['AS2-To']
assert_equal '"B O B"', headers['AS2-From']
end
end
end
end

0 comments on commit d15814c

Please sign in to comment.