Class: As2::Client
- Inherits:
-
Object
- Object
- As2::Client
- Defined in:
- lib/as2/client.rb,
lib/as2/client/result.rb
Defined Under Namespace
Classes: Result
Instance Attribute Summary collapse
-
#partner ⇒ Object
readonly
Returns the value of attribute partner.
-
#server_info ⇒ Object
readonly
Returns the value of attribute server_info.
Instance Method Summary collapse
- #as2_from ⇒ Object
- #as2_to ⇒ Object
- #evaluate_mdn(mdn_body:, mdn_content_type:, original_message_id:, original_body:) ⇒ Object
-
#initialize(partner, server_info: nil, logger: nil) ⇒ Client
constructor
A new instance of Client.
-
#send_file(file_name, content: nil, content_type: 'application/EDI-Consent') ⇒ As2::Client::Result
Send a file to a partner.
Constructor Details
#initialize(partner, server_info: nil, logger: nil) ⇒ Client
Returns a new instance of Client.
14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
# File 'lib/as2/client.rb', line 14 def initialize(partner, server_info: nil, logger: nil) @logger = logger || Logger.new('/dev/null') if partner.is_a?(As2::Config::Partner) @partner = partner else @partner = Config.partners[partner] unless @partner raise "Partner #{partner} is not registered" end end @server_info = server_info || Config.server_info end |
Instance Attribute Details
#partner ⇒ Object (readonly)
Returns the value of attribute partner.
5 6 7 |
# File 'lib/as2/client.rb', line 5 def partner @partner end |
#server_info ⇒ Object (readonly)
Returns the value of attribute server_info.
5 6 7 |
# File 'lib/as2/client.rb', line 5 def server_info @server_info end |
Instance Method Details
#as2_from ⇒ Object
33 34 35 |
# File 'lib/as2/client.rb', line 33 def as2_from @server_info.name end |
#as2_to ⇒ Object
29 30 31 |
# File 'lib/as2/client.rb', line 29 def as2_to @partner.name end |
#evaluate_mdn(mdn_body:, mdn_content_type:, original_message_id:, original_body:) ⇒ Object
126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 |
# File 'lib/as2/client.rb', line 126 def evaluate_mdn(mdn_body:, mdn_content_type:, original_message_id:, original_body:) report = { signature_verification_error: :not_checked, mic_matched: nil, mid_matched: nil, disposition: nil, plain_text_body: nil } # MDN bodies we've seen so far don't include Content-Type, which causes `read_smime` to fail. response_content = "Content-Type: #{mdn_content_type.to_s.strip}\r\n\r\n#{mdn_body}" if mdn_content_type.start_with?('multipart/signed') result = parse_signed_mdn( multipart_signed_message: response_content, certificate: @partner.certificate ) mdn_report = result[:mdn_report] report[:signature_verification_error] = result[:signature_verification_error] else # MDN may be unsigned if an error occurred, like if we sent an unrecognized As2-From header. mdn_report = Mail.new(response_content) end mdn_report.parts.each do |part| if part.content_type.start_with?('text/plain') report[:plain_text_body] = part.body.to_s.strip elsif part.content_type.start_with?('message/disposition-notification') # "The rules for constructing the AS2-disposition-notification content..." # https://datatracker.ietf.org/doc/html/rfc4130#section-7.4.3 = {} # TODO: can we use Mail built-ins for this? part.body.to_s.lines.each do |line| if line =~ /^([^:]+): (.+)$/ # downcase because we've seen both 'Disposition' and 'disposition' [$1.to_s.downcase] = $2 end end report[:disposition] = ['disposition'].strip report[:mid_matched] = == ['original-message-id'].strip if ['received-content-mic'] # do mic calc using the algorithm specified by server. # (even if we specify sha1, server may send back MIC using a different algo.) received_mic, micalg = ['received-content-mic'].split(',').map(&:strip) # if they don't specify, we'll use the algorithm we specified in the outbound transmission. # but it's only a guess & may fail. micalg ||= outbound_mic_algorithm mic = As2::DigestSelector.for_code(micalg).base64digest(original_body) report[:mic_matched] = received_mic == mic end end end report end |
#send_file(file_name, content: nil, content_type: 'application/EDI-Consent') ⇒ As2::Client::Result
Send a file to a partner
* If the content parameter is omitted, then `file_name` must be a path
to a local file, whose contents will be sent to the partner.
* If content parameter is specified, file_name is only used to tell the
partner the original name of the file.
49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 |
# File 'lib/as2/client.rb', line 49 def send_file(file_name, content: nil, content_type: 'application/EDI-Consent') outbound_mic_algorithm = 'sha256' = As2.(@server_info) 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['Subject'] = 'AS2 Transaction' req['Content-Type'] = 'application/pkcs7-mime; smime-type=enveloped-data; name=smime.p7m' req['Date'] = Time.now.rfc2822 req['Disposition-Notification-To'] = @server_info.url.to_s req['Disposition-Notification-Options'] = "signed-receipt-protocol=optional, pkcs7-signature; signed-receipt-micalg=optional, #{outbound_mic_algorithm}" req['Content-Disposition'] = 'attachment; filename="smime.p7m"' req['Recipient-Address'] = @partner.url.to_s req['Message-ID'] = document_content = content || File.read(file_name) document_payload = "Content-Type: #{content_type}\r\n" document_payload << "Content-Transfer-Encoding: base64\r\n" document_payload << "Content-Disposition: attachment; filename=#{file_name}\r\n" document_payload << "\r\n" document_payload << Base64.strict_encode64(document_content) signature = OpenSSL::PKCS7.sign @server_info.certificate, @server_info.pkey, document_payload signature.detached = true container = OpenSSL::PKCS7.write_smime signature, document_payload cipher = OpenSSL::Cipher::AES256.new(:CBC) # default, but we might have to make this configurable encrypted = OpenSSL::PKCS7.encrypt [@partner.certificate], container, cipher # > HTTP can handle binary data and so there is no need to use the # > content transfer encodings of MIME # # https://datatracker.ietf.org/doc/html/rfc4130#section-5.2.1 req.body = encrypted.to_der resp = nil exception = nil mdn_report = {} begin # 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' # http.set_debug_output $stderr # http.verify_mode = OpenSSL::SSL::VERIFY_NONE http.start do resp = http.request(req) end if resp && resp.code.start_with?('2') mdn_report = evaluate_mdn( mdn_content_type: resp['Content-Type'], mdn_body: resp.body, original_message_id: req['Message-ID'], original_body: document_payload ) end rescue => e exception = e end Result.new( response: resp, mic_matched: mdn_report[:mic_matched], mid_matched: mdn_report[:mid_matched], body: mdn_report[:plain_text_body], disposition: mdn_report[:disposition], signature_verification_error: mdn_report[:signature_verification_error], exception: exception, outbound_message_id: ) end |