as2 0.3.0 → 0.4.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 +4 -4
- data/.github/workflows/test.yml +34 -0
- data/.gitignore +1 -0
- data/CHANGELOG.md +15 -0
- data/Gemfile +9 -0
- data/README.md +34 -0
- data/as2.gemspec +5 -2
- data/lib/as2/client/result.rb +40 -0
- data/lib/as2/client.rb +108 -69
- data/lib/as2/config.rb +5 -0
- data/lib/as2/digest_selector.rb +23 -0
- data/lib/as2/message.rb +6 -2
- data/lib/as2/mime_generator.rb +1 -0
- data/lib/as2/server.rb +34 -26
- data/lib/as2/version.rb +1 -1
- data/lib/as2.rb +11 -0
- metadata +49 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: '094edd2c08516e42061e5623b49e4b2f355a3ca3a19a6ed17bc2764ea1073f9c'
|
4
|
+
data.tar.gz: c7678c44a070eda090eec89d3e3bacfdc2a1400b880bb39d52f2ba3e08ffe300
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
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
|
+
[](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", "
|
10
|
-
spec.email = ["development@officeluv.com", "
|
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
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
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
|
-
|
21
|
+
|
22
|
+
@server_info = server_info || Config.server_info
|
11
23
|
end
|
12
24
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
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
|
-
|
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
|
-
|
77
|
-
|
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
|
-
|
89
|
-
|
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
|
-
|
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
@@ -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
|
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.
|
90
|
+
mail.parts.find{|a| a.content_type == "application/edi-consent"}
|
87
91
|
else
|
88
92
|
mail
|
89
93
|
end
|
data/lib/as2/mime_generator.rb
CHANGED
data/lib/as2/server.rb
CHANGED
@@ -8,28 +8,36 @@ module As2
|
|
8
8
|
class Server
|
9
9
|
attr_accessor :logger
|
10
10
|
|
11
|
-
|
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
|
-
@
|
14
|
-
@
|
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'] != @
|
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
|
-
|
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, @
|
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 @
|
32
|
-
@
|
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' => @
|
81
|
-
'Original-Recipient' => "rfc822; #{@
|
82
|
-
'Final-Recipient' => "rfc822; #{@
|
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 @
|
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'] =
|
110
|
-
headers['AS2-From'] = @
|
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
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.
|
4
|
+
version: 0.4.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- OfficeLuv
|
8
|
-
-
|
8
|
+
- Alex Dean
|
9
9
|
autorequire:
|
10
10
|
bindir: exe
|
11
11
|
cert_chain: []
|
12
|
-
date:
|
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
|
-
-
|
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
|