as2 0.3.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3c28a94ec5915cd8930ba91814dea748017f86d0cbe2be94719fa54df5e6774f
4
- data.tar.gz: 47239349434164df4c1833561b6942e458f9b919415d221b9ae376b4a28755fe
3
+ metadata.gz: '094edd2c08516e42061e5623b49e4b2f355a3ca3a19a6ed17bc2764ea1073f9c'
4
+ data.tar.gz: c7678c44a070eda090eec89d3e3bacfdc2a1400b880bb39d52f2ba3e08ffe300
5
5
  SHA512:
6
- metadata.gz: b7f77c9176b2f279d5a678f72494f746b687432487df3b2c1d338754b89e5942e9af4798360a39528514cb826cc923148ac4a812cc64d65b7f9f4a104eb71b2f
7
- data.tar.gz: efdc7d01786e56ad1aabbbd9c373aa9d835bb299c775423df0470e622916efe080a0abc3b7bc6173e39b80223824d3c3bb885b6fe8d6b93405329dcaf9f82fd5
6
+ metadata.gz: c4ecd6838eb1e62174c584638141ba0f2af696bfd906765a0f3ffd4928e03ed3d203e9160c2ce5731439e282283fbf2b511b891719c98eeff262834b9bb1ec11
7
+ data.tar.gz: 852b03c4eebc90ea86447a4074afa417556b89f8613851eb59364dc6f8b4f9f1904452da3c4160929992c2263085774ff6c8cdf46345a92e75411daf945b8123
@@ -0,0 +1,34 @@
1
+ # This workflow uses actions that are not certified by GitHub.
2
+ # They are provided by a third-party and are governed by
3
+ # separate terms of service, privacy policy, and support
4
+ # documentation.
5
+ # This workflow will download a prebuilt Ruby version, install dependencies and run tests with Rake
6
+ # For more information see: https://github.com/marketplace/actions/setup-ruby-jruby-and-truffleruby
7
+
8
+ name: test suite
9
+
10
+ on:
11
+ push:
12
+ branches: '**'
13
+
14
+ jobs:
15
+ test:
16
+ strategy:
17
+ fail-fast: false
18
+ matrix:
19
+ ruby: ['2.5', '2.6', '2.7', '3.0', '3.1']
20
+
21
+ runs-on: ubuntu-latest
22
+
23
+ steps:
24
+ - uses: actions/checkout@v2
25
+ - name: Set up Ruby
26
+ # To automatically get bug fixes and new Ruby versions for ruby/setup-ruby,
27
+ # change this to (see https://github.com/ruby/setup-ruby#versioning):
28
+ uses: ruby/setup-ruby@v1
29
+ with:
30
+ ruby-version: ${{ matrix.ruby }}
31
+ - name: Install dependencies
32
+ run: bundle install
33
+ - name: Run tests
34
+ run: bundle exec rake test
data/.gitignore CHANGED
@@ -1,3 +1,4 @@
1
1
  certs
2
2
  .DS_Store
3
3
  Gemfile.lock
4
+ pkg
data/CHANGELOG.md CHANGED
@@ -1,9 +1,24 @@
1
+ ## 0.4.0, March 3, 2022
2
+
3
+ * client: correct MIC & signature verification when processing MDN response [#7](https://github.com/andjosh/as2/pull/7)
4
+ * also improves detection of successful & unsuccessful transmissions.
5
+ * client can transmit content which is not in a local file [#5](https://github.com/andjosh/as2/pull/5)
6
+ * also enables `As2::Client` and `As2::Server` can be used without reference to
7
+ the `As2::Config` global configuration.
8
+ * This allows certificate selection to be determined at runtime, making certificate
9
+ expiration & changeover much easier to orchestrate.
10
+
1
11
  ## 0.3.0, Dec 22, 2021
2
12
 
3
13
  * fix MIC calculation. [#1](https://github.com/andjosh/as2/pull/1)
4
14
  * allow loading of private key and certificates without local files. [#2](https://github.com/andjosh/as2/pull/2)
5
15
  * fix signature verification. [#3](https://github.com/andjosh/as2/pull/3)
6
16
 
17
+ ### breaking changes
18
+
19
+ * removed `As2::Message#original_message`
20
+ * removed `As2::Server::HEADER_MAP`
21
+
7
22
  ## prior to 0.3.0
8
23
 
9
24
  Initial work by [@andjosh](https://github.com/andjosh) and [@datanoise](https://github.com/datanoise).
data/Gemfile CHANGED
@@ -2,3 +2,12 @@ source 'https://rubygems.org'
2
2
 
3
3
  # Specify your gem's dependencies in as2.gemspec
4
4
  gemspec
5
+
6
+ # from https://github.com/rails/rails/pull/42308/files
7
+ if RUBY_VERSION >= "3.1"
8
+ # net-smtp, net-imap and net-pop were removed from default gems in Ruby 3.1, but is used by the `mail` gem.
9
+ # So we need to add them as dependencies until `mail` is fixed: https://github.com/mikel/mail/pull/1439
10
+ gem "net-smtp", require: false
11
+ gem "net-imap", require: false
12
+ gem "net-pop", require: false
13
+ end
data/README.md CHANGED
@@ -4,6 +4,40 @@ This is a proof of concept implementation of AS2 protocol: http://www.ietf.org/r
4
4
 
5
5
  Tested with the mendelson AS2 implementation from http://as2.mendelson-e-c.com
6
6
 
7
+ ## Build Status
8
+
9
+ [![Test Suite](https://github.com/andjosh/as2/actions/workflows/test.yml/badge.svg)](https://github.com/andjosh/as2/actions/workflows/test.yml)
10
+
11
+ ## Known Limitations
12
+
13
+ These limitations may be removed over time as demand (and pull requests!) come
14
+ along.
15
+
16
+ 1. RFC defines a number of optional features that partners can pick and choose
17
+ amongst. We currently have hard-coded options for many of these. Our current
18
+ choices are likely the most common ones in use, but we do not offer all the
19
+ configuration options needed for a fully-compliant implementation. https://datatracker.ietf.org/doc/html/rfc4130#section-2.4.2
20
+ 1. Encrypted or Unencrypted Data: We assume all messages are encrypted. An
21
+ error will result if partner sends us an unencrypted message.
22
+ 2. Signed or Unsigned Data: We error if partner sends an unsigned message.
23
+ Partners can request unsigned MDNs, but we always send signed MDNs.
24
+ 3. Optional Use of Receipt: We always send a receipt.
25
+ 4. Use of Synchronous or Asynchronous Receipts: We do not support asynchronous
26
+ delivery of MDNs.
27
+ 5. Security Formatting: We should be reasonably compliant here.
28
+ 6. Hash Function, Message Digest Choices: We currently always use sha1. If a
29
+ partner asks for a different algorithm, we'll always use sha1 and partner
30
+ will see a MIC verification failure. AS2 RFC specifically prefers sha1 and
31
+ mentions md5. Mendelson AS2 server supports a number of other algorithms.
32
+ (sha256, sha512, etc)
33
+ 2. Payload bodies (typically EDI files) can be binary or base64 encoded. We
34
+ error if the body is not base64-encoded.
35
+ 3. Payload bodies can have a few different mime types. We expect only
36
+ `application/EDI-Consent`. We're unable to receive content that has any other
37
+ mime type. https://datatracker.ietf.org/doc/html/rfc1767#section-1
38
+ 4. AS2 partners may agree to use separate certificates for data encryption and data signing.
39
+ We do not support separate certificates for these purposes.
40
+
7
41
  ## Installation
8
42
 
9
43
  Add this line to your application's Gemfile:
data/as2.gemspec CHANGED
@@ -6,8 +6,8 @@ require 'as2/version'
6
6
  Gem::Specification.new do |spec|
7
7
  spec.name = "as2"
8
8
  spec.version = As2::VERSION
9
- spec.authors = ["OfficeLuv", "Transfix"]
10
- spec.email = ["development@officeluv.com", "alexdean@transfix.io"]
9
+ spec.authors = ["OfficeLuv", "Alex Dean"]
10
+ spec.email = ["development@officeluv.com", "github@mostlyalex.com"]
11
11
 
12
12
  spec.summary = %q{Simple AS2 server and client implementation}
13
13
  spec.description = %q{Simple AS2 server and client implementation. Follows the AS2 implementation from http://as2.mendelson-e-c.com}
@@ -34,4 +34,7 @@ Gem::Specification.new do |spec|
34
34
  spec.add_development_dependency "rake", ">= 10.0"
35
35
  spec.add_development_dependency "thin"
36
36
  spec.add_development_dependency "minitest"
37
+ spec.add_development_dependency "minitest-focus"
38
+ spec.add_development_dependency "webmock"
39
+ spec.add_development_dependency "pry"
37
40
  end
@@ -0,0 +1,40 @@
1
+ module As2
2
+ class Client
3
+ class Result
4
+ attr_reader :response, :mic_matched, :mid_matched, :body, :disposition, :signature_verification_error, :exception, :outbound_message_id
5
+
6
+ def initialize(response:, mic_matched:, mid_matched:, body:, disposition:, signature_verification_error:, exception:, outbound_message_id:)
7
+ @response = response
8
+ @mic_matched = mic_matched
9
+ @mid_matched = mid_matched
10
+ @body = body
11
+ @disposition = disposition
12
+ @signature_verification_error = signature_verification_error
13
+ @exception = exception
14
+ @outbound_message_id = outbound_message_id
15
+ end
16
+
17
+ def signature_verified
18
+ self.signature_verification_error.nil?
19
+ end
20
+
21
+ # legacy name. accessor for backwards-compatibility.
22
+ def disp_code
23
+ self.disposition
24
+ end
25
+
26
+ def success
27
+ # 'processed' is good (though may include warnings.)
28
+ # 'processed/error' is not.
29
+ downcased_disposition = self.disposition.to_s.downcase
30
+
31
+ # TODO: we'll never have success if MDN is unsigned.
32
+ self.signature_verified &&
33
+ self.mic_matched &&
34
+ self.mid_matched &&
35
+ downcased_disposition.include?('processed') &&
36
+ !downcased_disposition.include?('processed/error')
37
+ end
38
+ end
39
+ end
40
+ end
data/lib/as2/client.rb CHANGED
@@ -2,96 +2,135 @@ require 'net/http'
2
2
 
3
3
  module As2
4
4
  class Client
5
- def initialize(partner_name)
6
- @partner = Config.partners[partner_name]
7
- unless @partner
8
- raise "Partner #{partner_name} is not registered"
5
+ attr_reader :partner, :server_info
6
+
7
+ # @param [As2::Config::Partner,String] partner The partner to send a message to.
8
+ # If a string is given, it should be a partner name which has been registered
9
+ # via a call to #add_partner.
10
+ # @param [As2::Config::ServerInfo,nil] server_info The server info used to identify
11
+ # this client to the partner. If omitted, the main As2::Config.server_info will be used.
12
+ def initialize(partner, server_info: nil)
13
+ if partner.is_a?(As2::Config::Partner)
14
+ @partner = partner
15
+ else
16
+ @partner = Config.partners[partner]
17
+ unless @partner
18
+ raise "Partner #{partner} is not registered"
19
+ end
9
20
  end
10
- @info = Config.server_info
21
+
22
+ @server_info = server_info || Config.server_info
11
23
  end
12
24
 
13
- Result = Struct.new :success, :response, :mic_matched, :mid_matched, :body, :disp_code
14
-
15
- def send_file(file_name)
16
- http = Net::HTTP.new(@partner.url.host, @partner.url.port)
17
- http.use_ssl = @partner.url.scheme == 'https'
18
- # http.set_debug_output $stderr
19
- http.start do
20
- req = Net::HTTP::Post.new @partner.url.path
21
- req['AS2-Version'] = '1.2'
22
- req['AS2-From'] = @info.name
23
- req['AS2-To'] = @partner.name
24
- req['Subject'] = 'AS2 EDI Transaction'
25
- req['Content-Type'] = 'application/pkcs7-mime; smime-type=enveloped-data; name=smime.p7m'
26
- req['Disposition-Notification-To'] = @info.url.to_s
27
- req['Disposition-Notification-Options'] = 'signed-receipt-protocol=optional, pkcs7-signature; signed-receipt-micalg=optional, sha1'
28
- req['Content-Disposition'] = 'attachment; filename="smime.p7m"'
29
- req['Recipient-Address'] = @info.url.to_s
30
- req['Content-Transfer-Encoding'] = 'base64'
31
- req['Message-ID'] = "<#{@info.name}-#{Time.now.strftime('%Y%m%d%H%M%S')}@#{@info.url.host}>"
32
-
33
- body = StringIO.new
34
- body.puts "Content-Type: application/EDI-Consent"
35
- body.puts "Content-Transfer-Encoding: base64"
36
- body.puts "Content-Disposition: attachment; filename=#{file_name}"
37
- body.puts
38
- body.puts [File.read(file_name)].pack("m*")
39
-
40
- mic = OpenSSL::Digest::SHA1.base64digest(body.string.gsub(/\n/, "\r\n"))
41
-
42
- pkcs7 = OpenSSL::PKCS7.sign @info.certificate, @info.pkey, body.string
43
- pkcs7.detached = true
44
- smime_signed = OpenSSL::PKCS7.write_smime pkcs7, body.string
45
- pkcs7 = OpenSSL::PKCS7.encrypt [@partner.certificate], smime_signed
46
- smime_encrypted = OpenSSL::PKCS7.write_smime pkcs7
47
-
48
- req.body = smime_encrypted.sub(/^.+?\n\n/m, '')
49
-
50
- resp = http.request(req)
51
-
52
- success = resp.code == '200'
53
- mic_matched = false
54
- mid_matched = false
55
- disp_code = nil
56
- body = nil
57
- if success
58
- body = resp.body
59
-
60
- smime = OpenSSL::PKCS7.read_smime "Content-Type: #{resp['Content-Type']}\r\n#{body}"
61
- smime.verify [@partner.certificate], Config.store
25
+ # Send a file to a partner
26
+ #
27
+ # * If the content parameter is omitted, then `file_name` must be a path
28
+ # to a local file, whose contents will be sent to the partner.
29
+ # * If content parameter is specified, file_name is only used to tell the
30
+ # partner the original name of the file.
31
+ #
32
+ # @param [String] file_name
33
+ # @param [String] content
34
+ # @return [As2::Client::Result]
35
+ def send_file(file_name, content: nil)
36
+ outbound_message_id = As2.generate_message_id(@server_info)
37
+
38
+ req = Net::HTTP::Post.new @partner.url.path
39
+ req['AS2-Version'] = '1.2'
40
+ req['AS2-From'] = @server_info.name
41
+ req['AS2-To'] = @partner.name
42
+ req['Subject'] = 'AS2 EDI Transaction'
43
+ req['Content-Type'] = 'application/pkcs7-mime; smime-type=enveloped-data; name=smime.p7m'
44
+ req['Disposition-Notification-To'] = @server_info.url.to_s
45
+ req['Disposition-Notification-Options'] = 'signed-receipt-protocol=optional, pkcs7-signature; signed-receipt-micalg=optional, sha1'
46
+ req['Content-Disposition'] = 'attachment; filename="smime.p7m"'
47
+ req['Recipient-Address'] = @server_info.url.to_s
48
+ req['Content-Transfer-Encoding'] = 'base64'
49
+ req['Message-ID'] = outbound_message_id
50
+
51
+ document_content = content || File.read(file_name)
52
+
53
+ document_payload = "Content-Type: application/EDI-Consent\r\n"
54
+ document_payload << "Content-Transfer-Encoding: base64\r\n"
55
+ document_payload << "Content-Disposition: attachment; filename=#{file_name}\r\n"
56
+ document_payload << "\r\n"
57
+ document_payload << Base64.strict_encode64(document_content)
58
+
59
+ signature = OpenSSL::PKCS7.sign @server_info.certificate, @server_info.pkey, document_payload
60
+ signature.detached = true
61
+ container = OpenSSL::PKCS7.write_smime signature, document_payload
62
+ encrypted = OpenSSL::PKCS7.encrypt [@partner.certificate], container
63
+ smime_encrypted = OpenSSL::PKCS7.write_smime encrypted
64
+
65
+ req.body = smime_encrypted.sub(/^.+?\n\n/m, '')
66
+
67
+ resp = nil
68
+ signature_verification_error = :not_checked
69
+ exception = nil
70
+ mic_matched = nil
71
+ mid_matched = nil
72
+ disposition = nil
73
+ plain_text_body = nil
74
+
75
+ begin
76
+ http = Net::HTTP.new(@partner.url.host, @partner.url.port)
77
+ http.use_ssl = @partner.url.scheme == 'https'
78
+ # http.set_debug_output $stderr
79
+ http.start do
80
+ resp = http.request(req)
81
+ end
82
+
83
+ if resp.code == '200'
84
+ response_content = "Content-Type: #{resp['Content-Type']}\r\n#{resp.body}"
85
+ smime = OpenSSL::PKCS7.read_smime response_content
86
+ # based on As2::Message version
87
+ # TODO: test cases based on valid/invalid responses. (response signed with wrong certificate, etc.)
88
+ smime.verify [@partner.certificate], OpenSSL::X509::Store.new, nil, OpenSSL::PKCS7::NOVERIFY | OpenSSL::PKCS7::NOINTERN
89
+ signature_verification_error = smime.error_string
62
90
 
63
91
  mail = Mail.new smime.data
64
92
  mail.parts.each do |part|
65
93
  case part.content_type
66
94
  when 'text/plain'
67
- body = part.body
95
+ plain_text_body = part.body
68
96
  when 'message/disposition-notification'
97
+ # "The rules for constructing the AS2-disposition-notification content..."
98
+ # https://datatracker.ietf.org/doc/html/rfc4130#section-7.4.3
99
+
69
100
  options = {}
101
+ # TODO: can we use Mail built-ins for this?
70
102
  part.body.to_s.lines.each do |line|
71
103
  if line =~ /^([^:]+): (.+)$/
72
104
  options[$1] = $2
73
105
  end
74
106
  end
75
107
 
76
- if req['Message-ID'] == options['Original-Message-ID']
77
- mid_matched = true
78
- else
79
- success = false
80
- end
81
-
82
- if options['Received-Content-MIC'].start_with?(mic)
83
- mic_matched = true
84
- else
85
- success = false
86
- end
108
+ disposition = options['Disposition']
109
+ mid_matched = req['Message-ID'] == options['Original-Message-ID']
87
110
 
88
- disp_code = options['Disposition']
89
- success = disp_code.end_with?('processed')
111
+ # do mic calc using the algorithm specified by server.
112
+ # (even if we specify sha1, server may send back MIC using a different algo.)
113
+ received_mic, micalg = options['Received-Content-MIC'].split(',').map(&:strip)
114
+ micalg ||= 'sha1'
115
+ mic = As2::DigestSelector.for_code(micalg).base64digest(document_payload)
116
+ mic_matched = received_mic == mic
90
117
  end
91
118
  end
92
119
  end
93
- Result.new success, resp, mic_matched, mid_matched, body, disp_code
120
+ rescue => e
121
+ exception = e
94
122
  end
123
+
124
+ Result.new(
125
+ response: resp,
126
+ mic_matched: mic_matched,
127
+ mid_matched: mid_matched,
128
+ body: plain_text_body,
129
+ disposition: disposition,
130
+ signature_verification_error: signature_verification_error,
131
+ exception: exception,
132
+ outbound_message_id: outbound_message_id
133
+ )
95
134
  end
96
135
  end
97
136
  end
data/lib/as2/config.rb CHANGED
@@ -97,6 +97,11 @@ module As2
97
97
  def store
98
98
  @store ||= OpenSSL::X509::Store.new
99
99
  end
100
+
101
+ def reset!
102
+ @partners = {}
103
+ @store = OpenSSL::X509::Store.new
104
+ end
100
105
  end
101
106
  end
102
107
  end
@@ -0,0 +1,23 @@
1
+ require 'openssl'
2
+
3
+ module As2
4
+ class DigestSelector
5
+ @map = {
6
+ 'sha1' => OpenSSL::Digest::SHA1,
7
+ 'sha256' => OpenSSL::Digest::SHA256,
8
+ 'sha384' => OpenSSL::Digest::SHA384,
9
+ 'sha512' => OpenSSL::Digest::SHA512,
10
+ 'md5' => OpenSSL::Digest::MD5
11
+ }
12
+
13
+ def self.valid_codes
14
+ @map.keys
15
+ end
16
+
17
+ def self.for_code(code)
18
+ normalized = code.strip.downcase.gsub(/[^a-z0-9]/, '')
19
+
20
+ @map[normalized] || OpenSSL::Digest::SHA1
21
+ end
22
+ end
23
+ end
data/lib/as2/message.rb CHANGED
@@ -29,7 +29,10 @@ module As2
29
29
  # https://datatracker.ietf.org/doc/html/rfc3851#section-3.4.3.1
30
30
 
31
31
  # TODO: more robust detection of content vs signature (if they're ever out of order).
32
- content = mail.parts[0].raw_source.strip
32
+ content = mail.parts[0].raw_source
33
+ # remove any leading \r\n characters (between headers & body i think).
34
+ content = content.gsub(/\A\s+/, '')
35
+
33
36
  signature = OpenSSL::PKCS7.new(mail.parts[1].body.to_s)
34
37
 
35
38
  # using an empty CA store. see notes on NOVERIFY flag below.
@@ -77,13 +80,14 @@ module As2
77
80
  end
78
81
 
79
82
  def mic
83
+ # TODO: could use As2::DigestSelector if a different algo is needed.
80
84
  OpenSSL::Digest::SHA1.base64digest(attachment.raw_source.strip)
81
85
  end
82
86
 
83
87
  # Return the attached file, use .filename and .body on the return value
84
88
  def attachment
85
89
  if mail.has_attachments?
86
- mail.attachments.find{|a| a.content_type == "application/edi-consent"}
90
+ mail.parts.find{|a| a.content_type == "application/edi-consent"}
87
91
  else
88
92
  mail
89
93
  end
@@ -5,6 +5,7 @@ module As2
5
5
  @parts = []
6
6
  @body = ""
7
7
  @headers = {}
8
+ @id = nil
8
9
  end
9
10
 
10
11
  def [](name)
data/lib/as2/server.rb CHANGED
@@ -8,28 +8,36 @@ module As2
8
8
  class Server
9
9
  attr_accessor :logger
10
10
 
11
- def initialize(options = {}, &block)
11
+ # @param [As2::Config::ServerInfo] server_info Config used for naming of this
12
+ # server and key/certificate selection. If omitted, the main As2::Config.server_info is used.
13
+ # @param [As2::Config::Partner] partner Which partner to receive messages from.
14
+ # If omitted, the partner is determined by incoming HTTP headers.
15
+ # @param [Proc] on_signature_failure A proc which will be called if signature verification fails.
16
+ # @param [Proc] block A proc which will be called with file_name and file content.
17
+ def initialize(server_info: nil, partner: nil, on_signature_failure: nil, &block)
12
18
  @block = block
13
- @info = Config.server_info
14
- @options = options
19
+ @server_info = server_info || Config.server_info
20
+ @partner = partner
21
+ @signature_failure_handler = on_signature_failure
15
22
  end
16
23
 
17
24
  def call(env)
18
- if env['HTTP_AS2_TO'] != @info.name
25
+ if env['HTTP_AS2_TO'] != @server_info.name
19
26
  return send_error(env, "Invalid destination name #{env['HTTP_AS2_TO']}")
20
27
  end
21
28
 
22
- partner = Config.partners[env['HTTP_AS2_FROM']]
23
- unless partner
29
+ partner = @partner || Config.partners[env['HTTP_AS2_FROM']]
30
+
31
+ if !partner || env['HTTP_AS2_FROM'] != partner.name
24
32
  return send_error(env, "Invalid partner name #{env['HTTP_AS2_FROM']}")
25
33
  end
26
34
 
27
35
  request = Rack::Request.new(env)
28
- message = Message.new(request.body.read, @info.pkey, @info.certificate)
36
+ message = Message.new(request.body.read, @server_info.pkey, @server_info.certificate)
29
37
 
30
38
  unless message.valid_signature?(partner.certificate)
31
- if @options[:on_signature_failure]
32
- @options[:on_signature_failure].call({
39
+ if @signature_failure_handler
40
+ @signature_failure_handler.call({
33
41
  env: env,
34
42
  smime_string: message.decrypted_message,
35
43
  verification_error: message.verification_error
@@ -50,17 +58,6 @@ module As2
50
58
  send_mdn(env, message.mic)
51
59
  end
52
60
 
53
- private
54
-
55
- def logger(env)
56
- @logger ||= Logger.new env['rack.errors']
57
- end
58
-
59
- def send_error(env, msg)
60
- logger(env).error msg
61
- send_mdn env, nil, msg
62
- end
63
-
64
61
  def send_mdn(env, mic, failed = nil)
65
62
  report = MimeGenerator::Part.new
66
63
  report['Content-Type'] = 'multipart/report; report-type=disposition-notification'
@@ -77,9 +74,9 @@ module As2
77
74
  notification['Content-Transfer-Encoding'] = '7bit'
78
75
 
79
76
  options = {
80
- 'Reporting-UA' => @info.name,
81
- 'Original-Recipient' => "rfc822; #{@info.name}",
82
- 'Final-Recipient' => "rfc822; #{@info.name}",
77
+ 'Reporting-UA' => @server_info.name,
78
+ 'Original-Recipient' => "rfc822; #{@server_info.name}",
79
+ 'Final-Recipient' => "rfc822; #{@server_info.name}",
83
80
  'Original-Message-ID' => env['HTTP_MESSAGE_ID']
84
81
  }
85
82
  if failed
@@ -96,7 +93,7 @@ module As2
96
93
 
97
94
  report.write msg_out
98
95
 
99
- pkcs7 = OpenSSL::PKCS7.sign @info.certificate, @info.pkey, msg_out.string
96
+ pkcs7 = OpenSSL::PKCS7.sign @server_info.certificate, @server_info.pkey, msg_out.string
100
97
  pkcs7.detached = true
101
98
  smime_signed = OpenSSL::PKCS7.write_smime pkcs7, msg_out.string
102
99
 
@@ -106,13 +103,24 @@ module As2
106
103
  headers = {}
107
104
  headers['Content-Type'] = content_type
108
105
  headers['MIME-Version'] = '1.0'
109
- headers['Message-ID'] = "<#{@info.name}-#{Time.now.strftime('%Y%m%d%H%M%S')}@#{@info.domain}>"
110
- headers['AS2-From'] = @info.name
106
+ headers['Message-ID'] = As2.generate_message_id(@server_info)
107
+ headers['AS2-From'] = @server_info.name
111
108
  headers['AS2-To'] = env['HTTP_AS2_FROM']
112
109
  headers['AS2-Version'] = '1.2'
113
110
  headers['Connection'] = 'close'
114
111
 
115
112
  [200, headers, ["\r\n" + smime_signed]]
116
113
  end
114
+
115
+ private
116
+
117
+ def logger(env)
118
+ @logger ||= Logger.new env['rack.errors']
119
+ end
120
+
121
+ def send_error(env, msg)
122
+ logger(env).error msg
123
+ send_mdn env, nil, msg
124
+ end
117
125
  end
118
126
  end
data/lib/as2/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module As2
2
- VERSION = "0.3.0"
2
+ VERSION = "0.4.0"
3
3
  end
data/lib/as2.rb CHANGED
@@ -1,12 +1,23 @@
1
1
  require 'openssl'
2
2
  require 'mail'
3
+ require 'securerandom'
3
4
  require 'as2/config'
4
5
  require 'as2/server'
5
6
  require 'as2/client'
7
+ require 'as2/client/result'
8
+ require 'as2/digest_selector'
6
9
  require "as2/version"
7
10
 
8
11
  module As2
9
12
  def self.configure(&block)
10
13
  Config.configure(&block)
11
14
  end
15
+
16
+ def self.reset_config!
17
+ Config.reset!
18
+ end
19
+
20
+ def self.generate_message_id(server_info)
21
+ "<#{server_info.name}-#{Time.now.strftime('%Y%m%d-%H%M%S')}-#{SecureRandom.uuid}@#{server_info.domain}>"
22
+ end
12
23
  end
metadata CHANGED
@@ -1,15 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: as2
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - OfficeLuv
8
- - Transfix
8
+ - Alex Dean
9
9
  autorequire:
10
10
  bindir: exe
11
11
  cert_chain: []
12
- date: 2021-12-22 00:00:00.000000000 Z
12
+ date: 2022-03-03 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: mail
@@ -95,15 +95,58 @@ dependencies:
95
95
  - - ">="
96
96
  - !ruby/object:Gem::Version
97
97
  version: '0'
98
+ - !ruby/object:Gem::Dependency
99
+ name: minitest-focus
100
+ requirement: !ruby/object:Gem::Requirement
101
+ requirements:
102
+ - - ">="
103
+ - !ruby/object:Gem::Version
104
+ version: '0'
105
+ type: :development
106
+ prerelease: false
107
+ version_requirements: !ruby/object:Gem::Requirement
108
+ requirements:
109
+ - - ">="
110
+ - !ruby/object:Gem::Version
111
+ version: '0'
112
+ - !ruby/object:Gem::Dependency
113
+ name: webmock
114
+ requirement: !ruby/object:Gem::Requirement
115
+ requirements:
116
+ - - ">="
117
+ - !ruby/object:Gem::Version
118
+ version: '0'
119
+ type: :development
120
+ prerelease: false
121
+ version_requirements: !ruby/object:Gem::Requirement
122
+ requirements:
123
+ - - ">="
124
+ - !ruby/object:Gem::Version
125
+ version: '0'
126
+ - !ruby/object:Gem::Dependency
127
+ name: pry
128
+ requirement: !ruby/object:Gem::Requirement
129
+ requirements:
130
+ - - ">="
131
+ - !ruby/object:Gem::Version
132
+ version: '0'
133
+ type: :development
134
+ prerelease: false
135
+ version_requirements: !ruby/object:Gem::Requirement
136
+ requirements:
137
+ - - ">="
138
+ - !ruby/object:Gem::Version
139
+ version: '0'
98
140
  description: Simple AS2 server and client implementation. Follows the AS2 implementation
99
141
  from http://as2.mendelson-e-c.com
100
142
  email:
101
143
  - development@officeluv.com
102
- - alexdean@transfix.io
144
+ - github@mostlyalex.com
103
145
  executables: []
104
146
  extensions: []
105
147
  extra_rdoc_files: []
106
148
  files:
149
+ - ".github/workflows/test.yml"
107
150
  - ".gitignore"
108
151
  - CHANGELOG.md
109
152
  - Gemfile
@@ -118,7 +161,9 @@ files:
118
161
  - lib/as2.rb
119
162
  - lib/as2/base64_helper.rb
120
163
  - lib/as2/client.rb
164
+ - lib/as2/client/result.rb
121
165
  - lib/as2/config.rb
166
+ - lib/as2/digest_selector.rb
122
167
  - lib/as2/message.rb
123
168
  - lib/as2/mime_generator.rb
124
169
  - lib/as2/server.rb