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 +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
|
+
[![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", "
|
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
|