as2 0.2.5 → 0.5.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 +2 -0
- data/CHANGELOG.md +30 -0
- data/Gemfile +9 -0
- data/README.md +34 -0
- data/as2.gemspec +8 -5
- data/lib/as2/client/result.rb +40 -0
- data/lib/as2/client.rb +172 -83
- data/lib/as2/config.rb +30 -10
- data/lib/as2/digest_selector.rb +24 -0
- data/lib/as2/message.rb +79 -20
- data/lib/as2/mime_generator.rb +1 -0
- data/lib/as2/server.rb +59 -67
- data/lib/as2/version.rb +1 -1
- data/lib/as2.rb +11 -0
- metadata +59 -13
- data/Gemfile.lock +0 -35
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 40b3957adcdaa34f7df9fe0ee83966f7076e80bc9dc9c02b8f27a4fb22395fda
|
4
|
+
data.tar.gz: 1d8002959f6b4afa70c94fd363c9d97c15b2f20a82ee96b1bc39ebba8a55972e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: bb04b23ab84f14e131e74e3a213e71ae5f3009f805af08b7cc1c88dc3798745639e4cffea1a85811df51a522776797f236da98c82ab84863dd5891c0b37fc243
|
7
|
+
data.tar.gz: a71c9ea6c4c82637103ae1353e5b54cee88ad4f52b388883b0b409c24722247bbd0ead903ca5ef00ba1df41403ab11f1d6b4b03f9d03f58af39ccb399848adf0
|
@@ -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
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
## 0.5.0, March 21, 2022
|
2
|
+
|
3
|
+
* improvements to `As2::Client`. improve compatibility with non-Mendelson AS2 servers. [#8](https://github.com/andjosh/as2/pull/8)
|
4
|
+
* improve MDN generation, especially when an error occurs. [#9](https://github.com/andjosh/as2/pull/9)
|
5
|
+
* successfully parse unsigned MDNs. [#10](https://github.com/andjosh/as2/pull/10)
|
6
|
+
|
7
|
+
## 0.4.0, March 3, 2022
|
8
|
+
|
9
|
+
* client: correct MIC & signature verification when processing MDN response [#7](https://github.com/andjosh/as2/pull/7)
|
10
|
+
* also improves detection of successful & unsuccessful transmissions.
|
11
|
+
* client can transmit content which is not in a local file [#5](https://github.com/andjosh/as2/pull/5)
|
12
|
+
* also enables `As2::Client` and `As2::Server` can be used without reference to
|
13
|
+
the `As2::Config` global configuration.
|
14
|
+
* This allows certificate selection to be determined at runtime, making certificate
|
15
|
+
expiration & changeover much easier to orchestrate.
|
16
|
+
|
17
|
+
## 0.3.0, Dec 22, 2021
|
18
|
+
|
19
|
+
* fix MIC calculation. [#1](https://github.com/andjosh/as2/pull/1)
|
20
|
+
* allow loading of private key and certificates without local files. [#2](https://github.com/andjosh/as2/pull/2)
|
21
|
+
* fix signature verification. [#3](https://github.com/andjosh/as2/pull/3)
|
22
|
+
|
23
|
+
### breaking changes
|
24
|
+
|
25
|
+
* removed `As2::Message#original_message`
|
26
|
+
* removed `As2::Server::HEADER_MAP`
|
27
|
+
|
28
|
+
## prior to 0.3.0
|
29
|
+
|
30
|
+
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 sha256. If a
|
29
|
+
partner asks for a different algorithm, we'll always use sha256 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,12 +6,12 @@ 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}
|
14
|
-
spec.homepage = "https://github.com/
|
14
|
+
spec.homepage = "https://github.com/andjosh/as2"
|
15
15
|
spec.license = "MIT"
|
16
16
|
|
17
17
|
# Prevent pushing this gem to RubyGems.org by setting 'allowed_push_host', or
|
@@ -30,8 +30,11 @@ Gem::Specification.new do |spec|
|
|
30
30
|
spec.add_dependency "mail"
|
31
31
|
spec.add_dependency "rack"
|
32
32
|
|
33
|
-
spec.add_development_dependency "bundler", "
|
34
|
-
spec.add_development_dependency "rake", "
|
33
|
+
spec.add_development_dependency "bundler", ">= 1.10"
|
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,185 @@ 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
|
20
|
+
end
|
21
|
+
|
22
|
+
@server_info = server_info || Config.server_info
|
23
|
+
end
|
24
|
+
|
25
|
+
def as2_to
|
26
|
+
@partner.name
|
27
|
+
end
|
28
|
+
|
29
|
+
def as2_from
|
30
|
+
@server_info.name
|
31
|
+
end
|
32
|
+
|
33
|
+
# Send a file to a partner
|
34
|
+
#
|
35
|
+
# * If the content parameter is omitted, then `file_name` must be a path
|
36
|
+
# to a local file, whose contents will be sent to the partner.
|
37
|
+
# * If content parameter is specified, file_name is only used to tell the
|
38
|
+
# partner the original name of the file.
|
39
|
+
#
|
40
|
+
# @param [String] file_name
|
41
|
+
# @param [String] content
|
42
|
+
# @param [String] content_type This is the MIME Content-Type describing the `content` param,
|
43
|
+
# and will be included in the SMIME payload. It is not the HTTP Content-Type.
|
44
|
+
# @return [As2::Client::Result]
|
45
|
+
def send_file(file_name, content: nil, content_type: 'application/EDI-Consent')
|
46
|
+
outbound_mic_algorithm = 'sha256'
|
47
|
+
outbound_message_id = As2.generate_message_id(@server_info)
|
48
|
+
|
49
|
+
req = Net::HTTP::Post.new @partner.url.path
|
50
|
+
req['AS2-Version'] = '1.0' # 1.1 includes compression support, which we dont implement.
|
51
|
+
req['AS2-From'] = as2_from
|
52
|
+
req['AS2-To'] = as2_to
|
53
|
+
req['Subject'] = 'AS2 Transaction'
|
54
|
+
req['Content-Type'] = 'application/pkcs7-mime; smime-type=enveloped-data; name=smime.p7m'
|
55
|
+
req['Date'] = Time.now.rfc2822
|
56
|
+
req['Disposition-Notification-To'] = @server_info.url.to_s
|
57
|
+
req['Disposition-Notification-Options'] = "signed-receipt-protocol=optional, pkcs7-signature; signed-receipt-micalg=optional, #{outbound_mic_algorithm}"
|
58
|
+
req['Content-Disposition'] = 'attachment; filename="smime.p7m"'
|
59
|
+
req['Recipient-Address'] = @partner.url.to_s
|
60
|
+
req['Message-ID'] = outbound_message_id
|
61
|
+
|
62
|
+
document_content = content || File.read(file_name)
|
63
|
+
|
64
|
+
document_payload = "Content-Type: #{content_type}\r\n"
|
65
|
+
document_payload << "Content-Transfer-Encoding: base64\r\n"
|
66
|
+
document_payload << "Content-Disposition: attachment; filename=#{file_name}\r\n"
|
67
|
+
document_payload << "\r\n"
|
68
|
+
document_payload << Base64.strict_encode64(document_content)
|
69
|
+
|
70
|
+
signature = OpenSSL::PKCS7.sign @server_info.certificate, @server_info.pkey, document_payload
|
71
|
+
signature.detached = true
|
72
|
+
container = OpenSSL::PKCS7.write_smime signature, document_payload
|
73
|
+
cipher = OpenSSL::Cipher::AES256.new(:CBC) # default, but we might have to make this configurable
|
74
|
+
encrypted = OpenSSL::PKCS7.encrypt [@partner.certificate], container, cipher
|
75
|
+
|
76
|
+
# > HTTP can handle binary data and so there is no need to use the
|
77
|
+
# > content transfer encodings of MIME
|
78
|
+
#
|
79
|
+
# https://datatracker.ietf.org/doc/html/rfc4130#section-5.2.1
|
80
|
+
req.body = encrypted.to_der
|
81
|
+
|
82
|
+
resp = nil
|
83
|
+
exception = nil
|
84
|
+
mdn_report = {}
|
85
|
+
|
86
|
+
begin
|
87
|
+
# note: to pass this traffic through a debugging proxy (like Charles)
|
88
|
+
# set ENV['http_proxy'].
|
89
|
+
http = Net::HTTP.new(@partner.url.host, @partner.url.port)
|
90
|
+
http.use_ssl = @partner.url.scheme == 'https'
|
91
|
+
# http.set_debug_output $stderr
|
92
|
+
# http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
93
|
+
|
94
|
+
http.start do
|
95
|
+
resp = http.request(req)
|
96
|
+
end
|
97
|
+
|
98
|
+
if resp.code == '200'
|
99
|
+
mdn_report = evaluate_mdn(
|
100
|
+
mdn_content_type: resp['Content-Type'],
|
101
|
+
mdn_body: resp.body,
|
102
|
+
original_message_id: req['Message-ID'],
|
103
|
+
original_body: document_payload
|
104
|
+
)
|
105
|
+
end
|
106
|
+
rescue => e
|
107
|
+
exception = e
|
9
108
|
end
|
10
|
-
|
109
|
+
|
110
|
+
Result.new(
|
111
|
+
response: resp,
|
112
|
+
mic_matched: mdn_report[:mic_matched],
|
113
|
+
mid_matched: mdn_report[:mid_matched],
|
114
|
+
body: mdn_report[:plain_text_body],
|
115
|
+
disposition: mdn_report[:disposition],
|
116
|
+
signature_verification_error: mdn_report[:signature_verification_error],
|
117
|
+
exception: exception,
|
118
|
+
outbound_message_id: outbound_message_id
|
119
|
+
)
|
11
120
|
end
|
12
121
|
|
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
|
-
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
|
62
|
-
|
63
|
-
mail = Mail.new smime.data
|
64
|
-
mail.parts.each do |part|
|
65
|
-
case part.content_type
|
66
|
-
when 'text/plain'
|
67
|
-
body = part.body
|
68
|
-
when 'message/disposition-notification'
|
69
|
-
options = {}
|
70
|
-
part.body.to_s.lines.each do |line|
|
71
|
-
if line =~ /^([^:]+): (.+)$/
|
72
|
-
options[$1] = $2
|
73
|
-
end
|
74
|
-
end
|
75
|
-
|
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
|
87
|
-
|
88
|
-
disp_code = options['Disposition']
|
89
|
-
success = disp_code.end_with?('processed')
|
122
|
+
def evaluate_mdn(mdn_body:, mdn_content_type:, original_message_id:, original_body:)
|
123
|
+
report = {
|
124
|
+
signature_verification_error: :not_checked,
|
125
|
+
mic_matched: nil,
|
126
|
+
mid_matched: nil,
|
127
|
+
disposition: nil,
|
128
|
+
plain_text_body: nil
|
129
|
+
}
|
130
|
+
|
131
|
+
# MDN bodies we've seen so far don't include Content-Type, which causes `read_smime` to fail.
|
132
|
+
response_content = "Content-Type: #{mdn_content_type.to_s.strip}\r\n\r\n#{mdn_body}"
|
133
|
+
|
134
|
+
if mdn_content_type.start_with?('multipart/signed')
|
135
|
+
smime = OpenSSL::PKCS7.read_smime(response_content)
|
136
|
+
|
137
|
+
# create mail instance before #verify call.
|
138
|
+
# `smime.data` is emptied if verification fails, which means we wouldn't know disposition & other details.
|
139
|
+
mail = Mail.new(smime.data)
|
140
|
+
|
141
|
+
# based on As2::Message version
|
142
|
+
# TODO: test cases based on valid/invalid responses. (response signed with wrong certificate, etc.)
|
143
|
+
smime.verify [@partner.certificate], OpenSSL::X509::Store.new, nil, OpenSSL::PKCS7::NOVERIFY | OpenSSL::PKCS7::NOINTERN
|
144
|
+
report[:signature_verification_error] = smime.error_string
|
145
|
+
else
|
146
|
+
# MDN may be unsigned if an error occurred, like if we sent an unrecognized As2-From header.
|
147
|
+
mail = Mail.new(response_content)
|
148
|
+
end
|
149
|
+
|
150
|
+
mail.parts.each do |part|
|
151
|
+
if part.content_type.start_with?('text/plain')
|
152
|
+
report[:plain_text_body] = part.body.to_s.strip
|
153
|
+
elsif part.content_type.start_with?('message/disposition-notification')
|
154
|
+
# "The rules for constructing the AS2-disposition-notification content..."
|
155
|
+
# https://datatracker.ietf.org/doc/html/rfc4130#section-7.4.3
|
156
|
+
|
157
|
+
options = {}
|
158
|
+
# TODO: can we use Mail built-ins for this?
|
159
|
+
part.body.to_s.lines.each do |line|
|
160
|
+
if line =~ /^([^:]+): (.+)$/
|
161
|
+
# downcase because we've seen both 'Disposition' and 'disposition'
|
162
|
+
options[$1.to_s.downcase] = $2
|
90
163
|
end
|
91
164
|
end
|
165
|
+
|
166
|
+
report[:disposition] = options['disposition']
|
167
|
+
report[:mid_matched] = original_message_id == options['original-message-id']
|
168
|
+
|
169
|
+
if options['received-content-mic']
|
170
|
+
# do mic calc using the algorithm specified by server.
|
171
|
+
# (even if we specify sha1, server may send back MIC using a different algo.)
|
172
|
+
received_mic, micalg = options['received-content-mic'].split(',').map(&:strip)
|
173
|
+
|
174
|
+
# if they don't specify, we'll use the algorithm we specified in the outbound transmission.
|
175
|
+
# but it's only a guess & may fail.
|
176
|
+
micalg ||= outbound_mic_algorithm
|
177
|
+
|
178
|
+
mic = As2::DigestSelector.for_code(micalg).base64digest(original_body)
|
179
|
+
report[:mic_matched] = received_mic == mic
|
180
|
+
end
|
92
181
|
end
|
93
|
-
Result.new success, resp, mic_matched, mid_matched, body, disp_code
|
94
182
|
end
|
183
|
+
report
|
95
184
|
end
|
96
185
|
end
|
97
186
|
end
|
data/lib/as2/config.rb
CHANGED
@@ -1,6 +1,17 @@
|
|
1
1
|
require 'uri'
|
2
2
|
module As2
|
3
3
|
module Config
|
4
|
+
def self.build_certificate(input)
|
5
|
+
if input.kind_of? OpenSSL::X509::Certificate
|
6
|
+
input
|
7
|
+
elsif input.kind_of? String
|
8
|
+
OpenSSL::X509::Certificate.new File.read(input)
|
9
|
+
else
|
10
|
+
raise ArgumentError, "Invalid certificate. Provide a string (file path)" \
|
11
|
+
" or an OpenSSL::X509::Certificate instance. Got a #{input.class} instead."
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
4
15
|
class Partner < Struct.new :name, :url, :certificate
|
5
16
|
def url=(url)
|
6
17
|
if url.kind_of? String
|
@@ -11,7 +22,7 @@ module As2
|
|
11
22
|
end
|
12
23
|
|
13
24
|
def certificate=(certificate)
|
14
|
-
self['certificate'] =
|
25
|
+
self['certificate'] = As2::Config.build_certificate(certificate)
|
15
26
|
end
|
16
27
|
end
|
17
28
|
|
@@ -25,11 +36,20 @@ module As2
|
|
25
36
|
end
|
26
37
|
|
27
38
|
def certificate=(certificate)
|
28
|
-
self['certificate'] =
|
39
|
+
self['certificate'] = As2::Config.build_certificate(certificate)
|
29
40
|
end
|
30
41
|
|
31
|
-
def pkey=(
|
32
|
-
|
42
|
+
def pkey=(input)
|
43
|
+
# looks like even though you OpenSSL::PKey.new, you still end up with
|
44
|
+
# an instance which is an OpenSSL::PKey::PKey.
|
45
|
+
if input.kind_of? OpenSSL::PKey::PKey
|
46
|
+
self['pkey'] = input
|
47
|
+
elsif input.kind_of? String
|
48
|
+
self['pkey'] = OpenSSL::PKey.read File.read(input)
|
49
|
+
else
|
50
|
+
raise ArgumentError, "Invalid private key. Provide a string (file path)" \
|
51
|
+
" or an OpenSSL::PKey instance. Got a #{input.class} instead."
|
52
|
+
end
|
33
53
|
end
|
34
54
|
|
35
55
|
def add_partner
|
@@ -67,12 +87,7 @@ module As2
|
|
67
87
|
unless @server_info.domain
|
68
88
|
raise 'Your domain name is required'
|
69
89
|
end
|
70
|
-
|
71
|
-
store.add_cert @server_info.certificate
|
72
|
-
rescue OpenSSL::X509::StoreError => err
|
73
|
-
# ignore duplicate certs
|
74
|
-
raise unless err.message == 'cert already in hash table'
|
75
|
-
end
|
90
|
+
store.add_cert @server_info.certificate
|
76
91
|
end
|
77
92
|
|
78
93
|
def partners
|
@@ -82,6 +97,11 @@ module As2
|
|
82
97
|
def store
|
83
98
|
@store ||= OpenSSL::X509::Store.new
|
84
99
|
end
|
100
|
+
|
101
|
+
def reset!
|
102
|
+
@partners = {}
|
103
|
+
@store = OpenSSL::X509::Store.new
|
104
|
+
end
|
85
105
|
end
|
86
106
|
end
|
87
107
|
end
|
@@ -0,0 +1,24 @@
|
|
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
|
+
# we may receive 'sha256', 'sha-256', or 'SHA256'.
|
19
|
+
normalized = code.strip.downcase.gsub(/[^a-z0-9]/, '')
|
20
|
+
|
21
|
+
@map[normalized] || OpenSSL::Digest::SHA1
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
data/lib/as2/message.rb
CHANGED
@@ -1,49 +1,108 @@
|
|
1
|
-
require 'as2/base64_helper'
|
2
|
-
|
3
1
|
module As2
|
4
2
|
class Message
|
5
|
-
attr_reader :
|
3
|
+
attr_reader :verification_error
|
6
4
|
|
7
5
|
def initialize(message, private_key, public_certificate)
|
8
|
-
|
6
|
+
# TODO: might need to use OpenSSL::PKCS7.read_smime rather than .new sometimes
|
7
|
+
@pkcs7 = OpenSSL::PKCS7.new(message)
|
9
8
|
@private_key = private_key
|
10
9
|
@public_certificate = public_certificate
|
10
|
+
@verification_error = nil
|
11
11
|
end
|
12
12
|
|
13
13
|
def decrypted_message
|
14
|
-
@decrypted_message ||=
|
14
|
+
@decrypted_message ||= @pkcs7.decrypt @private_key, @public_certificate
|
15
15
|
end
|
16
16
|
|
17
17
|
def valid_signature?(partner_certificate)
|
18
|
-
|
19
|
-
|
18
|
+
content_type = mail.header_fields.find { |h| h.name == 'Content-Type' }.content_type
|
19
|
+
if content_type == "multipart/signed"
|
20
|
+
# for a "multipart/signed" message, we will do 'detatched' signature
|
21
|
+
# verification, where we supply the data to be verified as the 3rd parameter
|
22
|
+
# to OpenSSL::PKCS7#verify. this is in keeping with how this content type
|
23
|
+
# is described in the S/MIME RFC.
|
24
|
+
#
|
25
|
+
# > The multipart/signed MIME type has two parts. The first part contains
|
26
|
+
# > the MIME entity that is signed; the second part contains the "detached signature"
|
27
|
+
# > CMS SignedData object in which the encapContentInfo eContent field is absent.
|
28
|
+
#
|
29
|
+
# https://datatracker.ietf.org/doc/html/rfc3851#section-3.4.3.1
|
30
|
+
|
31
|
+
# TODO: more robust detection of content vs signature (if they're ever out of order).
|
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
|
+
|
36
|
+
signature = OpenSSL::PKCS7.new(mail.parts[1].body.to_s)
|
37
|
+
|
38
|
+
# using an empty CA store. see notes on NOVERIFY flag below.
|
39
|
+
store = OpenSSL::X509::Store.new
|
40
|
+
|
41
|
+
# notes on verification proces and flags used
|
42
|
+
#
|
43
|
+
# ## NOINTERN
|
44
|
+
#
|
45
|
+
# > If PKCS7_NOINTERN is set the certificates in the message itself are
|
46
|
+
# > not searched when locating the signer's certificate. This means that
|
47
|
+
# > all the signers certificates must be in the certs parameter.
|
48
|
+
#
|
49
|
+
# > One application of PKCS7_NOINTERN is to only accept messages signed
|
50
|
+
# > by a small number of certificates. The acceptable certificates would
|
51
|
+
# > be passed in the certs parameter. In this case if the signer is not
|
52
|
+
# > one of the certificates supplied in certs then the verify will fail
|
53
|
+
# > because the signer cannot be found.
|
54
|
+
#
|
55
|
+
# https://www.openssl.org/docs/manmaster/man3/PKCS7_verify.html
|
56
|
+
#
|
57
|
+
# we want this so we can be sure that the `partner_certificate` we supply
|
58
|
+
# was actually used to sign the message. otherwise we could get a positive
|
59
|
+
# verification even if `partner_certificate` didn't sign the message
|
60
|
+
# we're checking.
|
61
|
+
#
|
62
|
+
# ## NOVERIFY
|
63
|
+
#
|
64
|
+
# > If PKCS7_NOVERIFY is set the signer's certificates are not chain verified.
|
65
|
+
#
|
66
|
+
# ie: we won't attempt to connect signer (in the first param) to a root
|
67
|
+
# CA (in `store`, which is empty). alternately, we could instead remove
|
68
|
+
# this flag, and add `partner_certificate` to `store`. but what's the point?
|
69
|
+
# we'd only be verifying that `partner_certificate` is connected to `partner_certificate`.
|
70
|
+
output = signature.verify([partner_certificate], store, content, OpenSSL::PKCS7::NOVERIFY | OpenSSL::PKCS7::NOINTERN)
|
20
71
|
|
21
|
-
|
22
|
-
|
23
|
-
|
72
|
+
# when `signature.verify` fails, signature.error_string will be populated.
|
73
|
+
@verification_error = signature.error_string
|
74
|
+
|
75
|
+
output
|
76
|
+
else
|
77
|
+
# TODO: how to log this?
|
78
|
+
false
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def mic
|
83
|
+
digest = As2::DigestSelector.for_code(mic_algorithm)
|
84
|
+
digest.base64digest(attachment.raw_source.strip)
|
85
|
+
end
|
86
|
+
|
87
|
+
def mic_algorithm
|
88
|
+
'sha256'
|
24
89
|
end
|
25
90
|
|
26
91
|
# Return the attached file, use .filename and .body on the return value
|
27
92
|
def attachment
|
28
93
|
if mail.has_attachments?
|
29
|
-
|
94
|
+
# TODO: match 'application/edi*', test with 'application/edi-x12'
|
95
|
+
# test also with "application/edi-consent; name=this_is_a_filename.txt"
|
96
|
+
mail.parts.find{ |a| a.content_type.match(/^application\/edi/) }
|
30
97
|
else
|
31
98
|
mail
|
32
99
|
end
|
33
100
|
end
|
34
101
|
|
35
102
|
private
|
103
|
+
|
36
104
|
def mail
|
37
105
|
@mail ||= Mail.new(decrypted_message)
|
38
106
|
end
|
39
|
-
|
40
|
-
def read_smime(smime)
|
41
|
-
OpenSSL::PKCS7.read_smime(smime)
|
42
|
-
end
|
43
|
-
|
44
|
-
def decrypt_smime(smime)
|
45
|
-
message = read_smime(smime)
|
46
|
-
message.decrypt @private_key, @public_certificate
|
47
|
-
end
|
48
107
|
end
|
49
108
|
end
|
data/lib/as2/mime_generator.rb
CHANGED
data/lib/as2/server.rb
CHANGED
@@ -2,50 +2,51 @@ require 'rack'
|
|
2
2
|
require 'logger'
|
3
3
|
require 'stringio'
|
4
4
|
require 'as2/mime_generator'
|
5
|
-
require 'as2/base64_helper'
|
6
5
|
require 'as2/message'
|
7
6
|
|
8
7
|
module As2
|
9
8
|
class Server
|
10
|
-
HEADER_MAP = {
|
11
|
-
'To' => 'HTTP_AS2_TO',
|
12
|
-
'From' => 'HTTP_AS2_FROM',
|
13
|
-
'Subject' => 'HTTP_SUBJECT',
|
14
|
-
'MIME-Version' => 'HTTP_MIME_VERSION',
|
15
|
-
'Content-Disposition' => 'HTTP_CONTENT_DISPOSITION',
|
16
|
-
'Content-Type' => 'CONTENT_TYPE',
|
17
|
-
}
|
18
|
-
|
19
9
|
attr_accessor :logger
|
20
10
|
|
21
|
-
|
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)
|
22
18
|
@block = block
|
23
|
-
@
|
24
|
-
@
|
19
|
+
@server_info = server_info || Config.server_info
|
20
|
+
@partner = partner
|
21
|
+
@signature_failure_handler = on_signature_failure
|
25
22
|
end
|
26
23
|
|
27
24
|
def call(env)
|
28
|
-
if env['HTTP_AS2_TO'] != @
|
25
|
+
if env['HTTP_AS2_TO'] != @server_info.name
|
29
26
|
return send_error(env, "Invalid destination name #{env['HTTP_AS2_TO']}")
|
30
27
|
end
|
31
28
|
|
32
|
-
partner = Config.partners[env['HTTP_AS2_FROM']]
|
33
|
-
|
29
|
+
partner = @partner || Config.partners[env['HTTP_AS2_FROM']]
|
30
|
+
|
31
|
+
if !partner || env['HTTP_AS2_FROM'] != partner.name
|
34
32
|
return send_error(env, "Invalid partner name #{env['HTTP_AS2_FROM']}")
|
35
33
|
end
|
36
34
|
|
37
|
-
|
38
|
-
message = Message.new(
|
35
|
+
request = Rack::Request.new(env)
|
36
|
+
message = Message.new(request.body.read, @server_info.pkey, @server_info.certificate)
|
37
|
+
|
39
38
|
unless message.valid_signature?(partner.certificate)
|
40
|
-
if @
|
41
|
-
@
|
39
|
+
if @signature_failure_handler
|
40
|
+
@signature_failure_handler.call({
|
41
|
+
env: env,
|
42
|
+
smime_string: message.decrypted_message,
|
43
|
+
verification_error: message.verification_error
|
44
|
+
})
|
42
45
|
else
|
43
46
|
raise "Could not verify signature"
|
44
47
|
end
|
45
48
|
end
|
46
49
|
|
47
|
-
mic = OpenSSL::Digest::SHA1.base64digest(message.decrypted_message)
|
48
|
-
|
49
50
|
if @block
|
50
51
|
begin
|
51
52
|
@block.call message.attachment.filename, message.attachment.body
|
@@ -54,62 +55,41 @@ module As2
|
|
54
55
|
end
|
55
56
|
end
|
56
57
|
|
57
|
-
send_mdn(env, mic)
|
58
|
+
send_mdn(env, message.mic, message.mic_algorithm)
|
58
59
|
end
|
59
60
|
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
smime_data = StringIO.new
|
61
|
+
def send_mdn(env, mic, mic_algorithm, failed = nil)
|
62
|
+
# rules for MDN construction are covered in
|
63
|
+
# https://datatracker.ietf.org/doc/html/rfc4130#section-7.4.2
|
64
64
|
|
65
|
-
|
66
|
-
|
65
|
+
options = {
|
66
|
+
'Reporting-UA' => @server_info.name,
|
67
|
+
'Original-Recipient' => "rfc822; #{@server_info.name}",
|
68
|
+
'Final-Recipient' => "rfc822; #{@server_info.name}",
|
69
|
+
'Original-Message-ID' => env['HTTP_MESSAGE_ID']
|
70
|
+
}
|
71
|
+
if failed
|
72
|
+
options['Disposition'] = 'automatic-action/MDN-sent-automatically; failed'
|
73
|
+
options['Failure'] = failed
|
74
|
+
text_body = "There was an error with the AS2 transmission.\r\n\r\n#{failed}"
|
75
|
+
else
|
76
|
+
options['Disposition'] = 'automatic-action/MDN-sent-automatically; processed'
|
77
|
+
text_body = "The AS2 message has been received successfully"
|
67
78
|
end
|
79
|
+
options['Received-Content-MIC'] = "#{mic}, #{mic_algorithm}" if mic
|
68
80
|
|
69
|
-
smime_data.puts 'Content-Transfer-Encoding: base64'
|
70
|
-
smime_data.puts
|
71
|
-
smime_data.puts Base64Helper.ensure_base64(request.body.read)
|
72
|
-
|
73
|
-
return smime_data.string
|
74
|
-
end
|
75
|
-
|
76
|
-
def logger(env)
|
77
|
-
@logger ||= Logger.new env['rack.errors']
|
78
|
-
end
|
79
|
-
|
80
|
-
def send_error(env, msg)
|
81
|
-
logger(env).error msg
|
82
|
-
send_mdn env, nil, msg
|
83
|
-
end
|
84
|
-
|
85
|
-
def send_mdn(env, mic, failed = nil)
|
86
81
|
report = MimeGenerator::Part.new
|
87
82
|
report['Content-Type'] = 'multipart/report; report-type=disposition-notification'
|
88
83
|
|
89
84
|
text = MimeGenerator::Part.new
|
90
85
|
text['Content-Type'] = 'text/plain'
|
91
86
|
text['Content-Transfer-Encoding'] = '7bit'
|
92
|
-
text.body =
|
93
|
-
|
87
|
+
text.body = text_body
|
94
88
|
report.add_part text
|
95
89
|
|
96
90
|
notification = MimeGenerator::Part.new
|
97
91
|
notification['Content-Type'] = 'message/disposition-notification'
|
98
92
|
notification['Content-Transfer-Encoding'] = '7bit'
|
99
|
-
|
100
|
-
options = {
|
101
|
-
'Reporting-UA' => @info.name,
|
102
|
-
'Original-Recipient' => "rfc822; #{@info.name}",
|
103
|
-
'Final-Recipient' => "rfc822; #{@info.name}",
|
104
|
-
'Original-Message-ID' => env['HTTP_MESSAGE_ID']
|
105
|
-
}
|
106
|
-
if failed
|
107
|
-
options['Disposition'] = 'automatic-action/MDN-sent-automatically; failed'
|
108
|
-
options['Failure'] = failed
|
109
|
-
else
|
110
|
-
options['Disposition'] = 'automatic-action/MDN-sent-automatically; processed'
|
111
|
-
end
|
112
|
-
options['Received-Content-MIC'] = "#{mic}, sha1" if mic
|
113
93
|
notification.body = options.map{|n, v| "#{n}: #{v}"}.join("\r\n")
|
114
94
|
report.add_part notification
|
115
95
|
|
@@ -117,23 +97,35 @@ module As2
|
|
117
97
|
|
118
98
|
report.write msg_out
|
119
99
|
|
120
|
-
pkcs7 = OpenSSL::PKCS7.sign @
|
100
|
+
pkcs7 = OpenSSL::PKCS7.sign @server_info.certificate, @server_info.pkey, msg_out.string
|
121
101
|
pkcs7.detached = true
|
122
102
|
smime_signed = OpenSSL::PKCS7.write_smime pkcs7, msg_out.string
|
123
103
|
|
124
104
|
content_type = smime_signed[/^Content-Type: (.+?)$/m, 1]
|
125
|
-
smime_signed.sub!(/\A.+?^(?=---)/m, '')
|
105
|
+
# smime_signed.sub!(/\A.+?^(?=---)/m, '')
|
126
106
|
|
127
107
|
headers = {}
|
128
108
|
headers['Content-Type'] = content_type
|
109
|
+
# TODO: if MIME-Version header is actually needed, should extract it out of smime_signed.
|
129
110
|
headers['MIME-Version'] = '1.0'
|
130
|
-
headers['Message-ID'] =
|
131
|
-
headers['AS2-From'] = @
|
111
|
+
headers['Message-ID'] = As2.generate_message_id(@server_info)
|
112
|
+
headers['AS2-From'] = @server_info.name
|
132
113
|
headers['AS2-To'] = env['HTTP_AS2_FROM']
|
133
|
-
headers['AS2-Version'] = '1.
|
114
|
+
headers['AS2-Version'] = '1.0'
|
134
115
|
headers['Connection'] = 'close'
|
135
116
|
|
136
117
|
[200, headers, ["\r\n" + smime_signed]]
|
137
118
|
end
|
119
|
+
|
120
|
+
private
|
121
|
+
|
122
|
+
def logger(env)
|
123
|
+
@logger ||= Logger.new env['rack.errors']
|
124
|
+
end
|
125
|
+
|
126
|
+
def send_error(env, msg)
|
127
|
+
logger(env).error msg
|
128
|
+
send_mdn env, nil, 'sha1', msg
|
129
|
+
end
|
138
130
|
end
|
139
131
|
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,14 +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.5.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- OfficeLuv
|
8
|
-
|
8
|
+
- Alex Dean
|
9
|
+
autorequire:
|
9
10
|
bindir: exe
|
10
11
|
cert_chain: []
|
11
|
-
date:
|
12
|
+
date: 2022-03-21 00:00:00.000000000 Z
|
12
13
|
dependencies:
|
13
14
|
- !ruby/object:Gem::Dependency
|
14
15
|
name: mail
|
@@ -42,28 +43,28 @@ dependencies:
|
|
42
43
|
name: bundler
|
43
44
|
requirement: !ruby/object:Gem::Requirement
|
44
45
|
requirements:
|
45
|
-
- - "
|
46
|
+
- - ">="
|
46
47
|
- !ruby/object:Gem::Version
|
47
48
|
version: '1.10'
|
48
49
|
type: :development
|
49
50
|
prerelease: false
|
50
51
|
version_requirements: !ruby/object:Gem::Requirement
|
51
52
|
requirements:
|
52
|
-
- - "
|
53
|
+
- - ">="
|
53
54
|
- !ruby/object:Gem::Version
|
54
55
|
version: '1.10'
|
55
56
|
- !ruby/object:Gem::Dependency
|
56
57
|
name: rake
|
57
58
|
requirement: !ruby/object:Gem::Requirement
|
58
59
|
requirements:
|
59
|
-
- - "
|
60
|
+
- - ">="
|
60
61
|
- !ruby/object:Gem::Version
|
61
62
|
version: '10.0'
|
62
63
|
type: :development
|
63
64
|
prerelease: false
|
64
65
|
version_requirements: !ruby/object:Gem::Requirement
|
65
66
|
requirements:
|
66
|
-
- - "
|
67
|
+
- - ">="
|
67
68
|
- !ruby/object:Gem::Version
|
68
69
|
version: '10.0'
|
69
70
|
- !ruby/object:Gem::Dependency
|
@@ -94,17 +95,61 @@ dependencies:
|
|
94
95
|
- - ">="
|
95
96
|
- !ruby/object:Gem::Version
|
96
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'
|
97
140
|
description: Simple AS2 server and client implementation. Follows the AS2 implementation
|
98
141
|
from http://as2.mendelson-e-c.com
|
99
142
|
email:
|
100
143
|
- development@officeluv.com
|
144
|
+
- github@mostlyalex.com
|
101
145
|
executables: []
|
102
146
|
extensions: []
|
103
147
|
extra_rdoc_files: []
|
104
148
|
files:
|
149
|
+
- ".github/workflows/test.yml"
|
105
150
|
- ".gitignore"
|
151
|
+
- CHANGELOG.md
|
106
152
|
- Gemfile
|
107
|
-
- Gemfile.lock
|
108
153
|
- LICENSE.txt
|
109
154
|
- README.md
|
110
155
|
- Rakefile
|
@@ -116,17 +161,19 @@ files:
|
|
116
161
|
- lib/as2.rb
|
117
162
|
- lib/as2/base64_helper.rb
|
118
163
|
- lib/as2/client.rb
|
164
|
+
- lib/as2/client/result.rb
|
119
165
|
- lib/as2/config.rb
|
166
|
+
- lib/as2/digest_selector.rb
|
120
167
|
- lib/as2/message.rb
|
121
168
|
- lib/as2/mime_generator.rb
|
122
169
|
- lib/as2/server.rb
|
123
170
|
- lib/as2/version.rb
|
124
|
-
homepage: https://github.com/
|
171
|
+
homepage: https://github.com/andjosh/as2
|
125
172
|
licenses:
|
126
173
|
- MIT
|
127
174
|
metadata:
|
128
175
|
allowed_push_host: https://rubygems.org/
|
129
|
-
post_install_message:
|
176
|
+
post_install_message:
|
130
177
|
rdoc_options: []
|
131
178
|
require_paths:
|
132
179
|
- lib
|
@@ -141,9 +188,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
141
188
|
- !ruby/object:Gem::Version
|
142
189
|
version: '0'
|
143
190
|
requirements: []
|
144
|
-
|
145
|
-
|
146
|
-
signing_key:
|
191
|
+
rubygems_version: 3.1.4
|
192
|
+
signing_key:
|
147
193
|
specification_version: 4
|
148
194
|
summary: Simple AS2 server and client implementation
|
149
195
|
test_files: []
|
data/Gemfile.lock
DELETED
@@ -1,35 +0,0 @@
|
|
1
|
-
PATH
|
2
|
-
remote: .
|
3
|
-
specs:
|
4
|
-
as2 (0.2.4)
|
5
|
-
mail
|
6
|
-
rack
|
7
|
-
|
8
|
-
GEM
|
9
|
-
remote: https://rubygems.org/
|
10
|
-
specs:
|
11
|
-
daemons (1.2.3)
|
12
|
-
eventmachine (1.0.8)
|
13
|
-
mail (2.6.3)
|
14
|
-
mime-types (>= 1.16, < 3)
|
15
|
-
mime-types (2.6.2)
|
16
|
-
minitest (5.8.1)
|
17
|
-
rack (1.6.4)
|
18
|
-
rake (10.4.2)
|
19
|
-
thin (1.6.4)
|
20
|
-
daemons (~> 1.0, >= 1.0.9)
|
21
|
-
eventmachine (~> 1.0, >= 1.0.4)
|
22
|
-
rack (~> 1.0)
|
23
|
-
|
24
|
-
PLATFORMS
|
25
|
-
ruby
|
26
|
-
|
27
|
-
DEPENDENCIES
|
28
|
-
as2!
|
29
|
-
bundler (~> 1.10)
|
30
|
-
minitest
|
31
|
-
rake (~> 10.0)
|
32
|
-
thin
|
33
|
-
|
34
|
-
BUNDLED WITH
|
35
|
-
1.10.6
|