as2 0.2.4
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 +7 -0
- data/.gitignore +2 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +35 -0
- data/LICENSE.txt +21 -0
- data/README.md +60 -0
- data/Rakefile +10 -0
- data/as2.gemspec +37 -0
- data/bin/console +14 -0
- data/bin/setup +7 -0
- data/examples/client.rb +19 -0
- data/examples/server.rb +32 -0
- data/lib/as2.rb +12 -0
- data/lib/as2/base64_helper.rb +36 -0
- data/lib/as2/client.rb +97 -0
- data/lib/as2/config.rb +82 -0
- data/lib/as2/message.rb +49 -0
- data/lib/as2/mime_generator.rb +70 -0
- data/lib/as2/server.rb +139 -0
- data/lib/as2/version.rb +3 -0
- metadata +149 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 7cb3eb0153dae7ea4c9ddba6a2843c86982fb6a035bddf9f0638655d0e3c3cda
|
4
|
+
data.tar.gz: b4a7d09a451eb131ca1febc572e243144014029c9ac82a35ea43da12db5d3519
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: ea8bb601045953e40aca4a13193f8377b2fdeacae2d0b143adfda5e434025d7ed38b7db3aca8e11612f38cb431fab80d590d13923c29a52c51ea118c2b2c2ce1
|
7
|
+
data.tar.gz: aa7b396aa6de6271ae86bc8094ed72f7dce59ed996198f1f31f21c3438de585cee5083a466720e593f80c85474f24fb276f31faa0be8b70813248efc8129db8d
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,35 @@
|
|
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
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2015 Andrew Fecheyr
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
# As2
|
2
|
+
|
3
|
+
This is a proof of concept implementation of AS2 protocol: http://www.ietf.org/rfc/rfc4130.txt.
|
4
|
+
|
5
|
+
Tested with the mendelson AS2 implementation from http://as2.mendelson-e-c.com
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
|
9
|
+
Add this line to your application's Gemfile:
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
gem 'as2'
|
13
|
+
```
|
14
|
+
|
15
|
+
And then execute:
|
16
|
+
|
17
|
+
$ bundle
|
18
|
+
|
19
|
+
Or install it yourself as:
|
20
|
+
|
21
|
+
$ gem install as2
|
22
|
+
|
23
|
+
## Usage
|
24
|
+
|
25
|
+
Generate self signed server certificate:
|
26
|
+
|
27
|
+
### One step
|
28
|
+
|
29
|
+
`openssl req -x509 -newkey rsa:2048 -nodes -keyout server.key -out server.crt -days 365`
|
30
|
+
|
31
|
+
### Multi step
|
32
|
+
|
33
|
+
1. Generate a key ` openssl genrsa -des3 -out server.key 1024 `
|
34
|
+
2. Copy the protected key ` cp server.key server.key.org `
|
35
|
+
3. Remove the passphrase ` openssl rsa -in server.key.org -out server.key `
|
36
|
+
4. Generate signing request ` openssl req -new -key server.key -out server.csr `
|
37
|
+
5. Sign the request with your key ` openssl x509 -req -days 365 -in server.csr -signkey server.key -out server.crt `
|
38
|
+
|
39
|
+
## Development
|
40
|
+
|
41
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
42
|
+
|
43
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
44
|
+
|
45
|
+
You can run a local server with `bundle exec ruby examples/server.rb` and send it a file with `bundle exec ruby examples/client.rb <file>`. You may need to generate new certificates under `test/certificates` first (using `localhost` as your common name).
|
46
|
+
|
47
|
+
## Contributing
|
48
|
+
|
49
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/as2.
|
50
|
+
|
51
|
+
|
52
|
+
## License
|
53
|
+
|
54
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
55
|
+
|
56
|
+
## Acknowledgments
|
57
|
+
|
58
|
+
Original implementation by:
|
59
|
+
- [andruby](https://github.com/andruby)
|
60
|
+
- [datanoise](https://github.com/datanoise)
|
data/Rakefile
ADDED
data/as2.gemspec
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'as2/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "as2"
|
8
|
+
spec.version = As2::VERSION
|
9
|
+
spec.authors = ["OfficeLuv"]
|
10
|
+
spec.email = ["development@officeluv.com"]
|
11
|
+
|
12
|
+
spec.summary = %q{Simple AS2 server and client implementation}
|
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/officeluv/as2"
|
15
|
+
spec.license = "MIT"
|
16
|
+
|
17
|
+
# Prevent pushing this gem to RubyGems.org by setting 'allowed_push_host', or
|
18
|
+
# delete this section to allow pushing this gem to any host.
|
19
|
+
if spec.respond_to?(:metadata)
|
20
|
+
spec.metadata['allowed_push_host'] = 'https://rubygems.org/'
|
21
|
+
else
|
22
|
+
raise "RubyGems 2.0 or newer is required to protect against public gem pushes."
|
23
|
+
end
|
24
|
+
|
25
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
26
|
+
spec.bindir = "exe"
|
27
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
28
|
+
spec.require_paths = ["lib"]
|
29
|
+
|
30
|
+
spec.add_dependency "mail"
|
31
|
+
spec.add_dependency "rack"
|
32
|
+
|
33
|
+
spec.add_development_dependency "bundler", "~> 1.10"
|
34
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
35
|
+
spec.add_development_dependency "thin"
|
36
|
+
spec.add_development_dependency "minitest"
|
37
|
+
end
|
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "as2"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start
|
data/bin/setup
ADDED
data/examples/client.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'as2'
|
2
|
+
# require 'pry'
|
3
|
+
|
4
|
+
As2.configure do |conf|
|
5
|
+
conf.name = 'MyClient'
|
6
|
+
conf.url = 'http://localhost:8080/as2/HttpReceiver'
|
7
|
+
conf.certificate = 'test/certificates/client.crt'
|
8
|
+
conf.pkey = 'test/certificates/client.key'
|
9
|
+
conf.domain = 'mydomain.com'
|
10
|
+
conf.add_partner do |partner|
|
11
|
+
partner.name = 'MyServer'
|
12
|
+
partner.url = 'http://localhost:3000/as2'
|
13
|
+
partner.certificate = 'test/certificates/server.crt'
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
client = As2::Client.new 'MyServer'
|
18
|
+
result = client.send_file(ARGV.first)
|
19
|
+
p result
|
data/examples/server.rb
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'as2'
|
2
|
+
require 'rack'
|
3
|
+
|
4
|
+
As2.configure do |conf|
|
5
|
+
conf.name = 'MyServer'
|
6
|
+
conf.url = 'http://localhost:3000/as2'
|
7
|
+
conf.certificate = 'test/certificates/server.crt'
|
8
|
+
conf.pkey = 'test/certificates/server.key'
|
9
|
+
conf.domain = 'mydomain.com'
|
10
|
+
conf.add_partner do |partner|
|
11
|
+
partner.name = 'MyClient'
|
12
|
+
partner.url = 'http://localhost:8080/as2/HttpReceiver'
|
13
|
+
partner.certificate = 'test/certificates/client.crt'
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
handler = As2::Server.new do |filename, body|
|
18
|
+
puts "SUCCESSFUL DOWNLOAD"
|
19
|
+
puts "FILENAME: #{filename}"
|
20
|
+
puts
|
21
|
+
puts body
|
22
|
+
end
|
23
|
+
|
24
|
+
builder = Rack::Builder.new do
|
25
|
+
use Rack::CommonLogger
|
26
|
+
map '/as2' do
|
27
|
+
run handler
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
puts "As2 version: #{As2::VERSION}"
|
32
|
+
Rack::Handler::Thin.run builder, Port: 3000
|
data/lib/as2.rb
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'base64'
|
2
|
+
|
3
|
+
module As2
|
4
|
+
module Base64Helper
|
5
|
+
# Will base64 encoded string, unless it already is base64 encoded
|
6
|
+
def self.ensure_base64(string)
|
7
|
+
begin
|
8
|
+
# If string is not base64 encoded, this will raise an ArgumentError
|
9
|
+
Base64.strict_decode64(string.gsub("\n",""))
|
10
|
+
return string
|
11
|
+
rescue ArgumentError
|
12
|
+
# The string is not yet base64 encoded
|
13
|
+
return Base64.encode64(string)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
# If the multipart body is binary encoded, replace it with base64 encoded version
|
18
|
+
def self.ensure_body_base64(multipart)
|
19
|
+
boundary = multipart.scan(/boundary="([^"]*)"/)[0][0]
|
20
|
+
boundary_split = Regexp.escape("--#{boundary}")
|
21
|
+
parts = multipart.split(/^#{boundary_split}-*\s*$/)
|
22
|
+
signature = parts[2]
|
23
|
+
transfer_encoding = signature.scan(/Content-Transfer-Encoding: (.*)/)[0][0].strip
|
24
|
+
if transfer_encoding == 'binary'
|
25
|
+
header, body = signature.split(/^\s*$/,2).map(&:lstrip)
|
26
|
+
body_base64 = Base64.encode64(body)
|
27
|
+
new_header = header.sub('Content-Transfer-Encoding: binary', 'Content-Transfer-Encoding: base64')
|
28
|
+
parts[2] = new_header + "\r\n" + body_base64
|
29
|
+
new_multipart = parts.join("--#{boundary}\r\n") + "--#{boundary}--\r\n"
|
30
|
+
return new_multipart
|
31
|
+
else
|
32
|
+
return multipart
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
data/lib/as2/client.rb
ADDED
@@ -0,0 +1,97 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
|
3
|
+
module As2
|
4
|
+
class Client
|
5
|
+
def initialize(partner_name)
|
6
|
+
@partner = Config.partners[partner_name]
|
7
|
+
unless @partner
|
8
|
+
raise "Partner #{partner_name} is not registered"
|
9
|
+
end
|
10
|
+
@info = Config.server_info
|
11
|
+
end
|
12
|
+
|
13
|
+
Result = Struct.new :success, :response, :mic_matched, :mid_matched, :body, :disp_code
|
14
|
+
|
15
|
+
def send_file(file_name)
|
16
|
+
http = Net::HTTP.new(@partner.url.host, @partner.url.port)
|
17
|
+
http.use_ssl = @partner.url.scheme == 'https'
|
18
|
+
# http.set_debug_output $stderr
|
19
|
+
http.start do
|
20
|
+
req = Net::HTTP::Post.new @partner.url.path
|
21
|
+
req['AS2-Version'] = '1.2'
|
22
|
+
req['AS2-From'] = @info.name
|
23
|
+
req['AS2-To'] = @partner.name
|
24
|
+
req['Subject'] = 'AS2 EDI Transaction'
|
25
|
+
req['Content-Type'] = 'application/pkcs7-mime; smime-type=enveloped-data; name=smime.p7m'
|
26
|
+
req['Disposition-Notification-To'] = @info.url.to_s
|
27
|
+
req['Disposition-Notification-Options'] = 'signed-receipt-protocol=optional, pkcs7-signature; signed-receipt-micalg=optional, sha1'
|
28
|
+
req['Content-Disposition'] = 'attachment; filename="smime.p7m"'
|
29
|
+
req['Recipient-Address'] = @info.url.to_s
|
30
|
+
req['Content-Transfer-Encoding'] = 'base64'
|
31
|
+
req['Message-ID'] = "<#{@info.name}-#{Time.now.strftime('%Y%m%d%H%M%S')}@#{@info.url.host}>"
|
32
|
+
|
33
|
+
body = StringIO.new
|
34
|
+
body.puts "Content-Type: application/EDI-Consent"
|
35
|
+
body.puts "Content-Transfer-Encoding: base64"
|
36
|
+
body.puts "Content-Disposition: attachment; filename=#{file_name}"
|
37
|
+
body.puts
|
38
|
+
body.puts [File.read(file_name)].pack("m*")
|
39
|
+
|
40
|
+
mic = OpenSSL::Digest::SHA1.base64digest(body.string.gsub(/\n/, "\r\n"))
|
41
|
+
|
42
|
+
pkcs7 = OpenSSL::PKCS7.sign @info.certificate, @info.pkey, body.string
|
43
|
+
pkcs7.detached = true
|
44
|
+
smime_signed = OpenSSL::PKCS7.write_smime pkcs7, body.string
|
45
|
+
pkcs7 = OpenSSL::PKCS7.encrypt [@partner.certificate], smime_signed
|
46
|
+
smime_encrypted = OpenSSL::PKCS7.write_smime pkcs7
|
47
|
+
|
48
|
+
req.body = smime_encrypted.sub(/^.+?\n\n/m, '')
|
49
|
+
|
50
|
+
resp = http.request(req)
|
51
|
+
|
52
|
+
success = resp.code == '200'
|
53
|
+
mic_matched = false
|
54
|
+
mid_matched = false
|
55
|
+
disp_code = nil
|
56
|
+
body = nil
|
57
|
+
if success
|
58
|
+
body = resp.body
|
59
|
+
|
60
|
+
smime = OpenSSL::PKCS7.read_smime "Content-Type: #{resp['Content-Type']}\r\n#{body}"
|
61
|
+
smime.verify [@partner.certificate], Config.store
|
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')
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
Result.new success, resp, mic_matched, mid_matched, body, disp_code
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
data/lib/as2/config.rb
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
require 'uri'
|
2
|
+
module As2
|
3
|
+
module Config
|
4
|
+
class Partner < Struct.new :name, :url, :certificate
|
5
|
+
def url=(url)
|
6
|
+
if url.kind_of? String
|
7
|
+
self['url'] = URI.parse url
|
8
|
+
else
|
9
|
+
self['url'] = url
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def certificate=(certificate)
|
14
|
+
self['certificate'] = OpenSSL::X509::Certificate.new File.read(certificate)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
class ServerInfo < Struct.new :name, :url, :certificate, :pkey, :domain
|
19
|
+
def url=(url)
|
20
|
+
if url.kind_of? String
|
21
|
+
self['url'] = URI.parse url
|
22
|
+
else
|
23
|
+
self['url'] = url
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def certificate=(certificate)
|
28
|
+
self['certificate'] = OpenSSL::X509::Certificate.new File.read(certificate)
|
29
|
+
end
|
30
|
+
|
31
|
+
def pkey=(pkey)
|
32
|
+
self['pkey'] = OpenSSL::PKey.read File.read(pkey)
|
33
|
+
end
|
34
|
+
|
35
|
+
def add_partner
|
36
|
+
partner = Partner.new
|
37
|
+
yield partner
|
38
|
+
unless partner.name
|
39
|
+
raise 'Partner name is required'
|
40
|
+
end
|
41
|
+
unless partner.certificate
|
42
|
+
raise 'Partner certificate is required'
|
43
|
+
end
|
44
|
+
unless partner.url
|
45
|
+
raise 'Partner URL is required'
|
46
|
+
end
|
47
|
+
Config.partners[partner.name] = partner
|
48
|
+
Config.store.add_cert partner.certificate
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
class << self
|
53
|
+
attr_reader :server_info
|
54
|
+
|
55
|
+
def configure
|
56
|
+
@server_info ||= ServerInfo.new
|
57
|
+
yield @server_info
|
58
|
+
unless @server_info.name
|
59
|
+
raise 'Your Partner name is required'
|
60
|
+
end
|
61
|
+
unless @server_info.certificate
|
62
|
+
raise 'Your certificate is required'
|
63
|
+
end
|
64
|
+
unless @server_info.url
|
65
|
+
raise 'Your URL is required'
|
66
|
+
end
|
67
|
+
unless @server_info.domain
|
68
|
+
raise 'Your domain name is required'
|
69
|
+
end
|
70
|
+
store.add_cert @server_info.certificate
|
71
|
+
end
|
72
|
+
|
73
|
+
def partners
|
74
|
+
@partners ||= {}
|
75
|
+
end
|
76
|
+
|
77
|
+
def store
|
78
|
+
@store ||= OpenSSL::X509::Store.new
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
data/lib/as2/message.rb
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'as2/base64_helper'
|
2
|
+
|
3
|
+
module As2
|
4
|
+
class Message
|
5
|
+
attr_reader :original_message
|
6
|
+
|
7
|
+
def initialize(message, private_key, public_certificate)
|
8
|
+
@original_message = message
|
9
|
+
@private_key = private_key
|
10
|
+
@public_certificate = public_certificate
|
11
|
+
end
|
12
|
+
|
13
|
+
def decrypted_message
|
14
|
+
@decrypted_message ||= decrypt_smime(original_message)
|
15
|
+
end
|
16
|
+
|
17
|
+
def valid_signature?(partner_certificate)
|
18
|
+
store = OpenSSL::X509::Store.new
|
19
|
+
store.add_cert(partner_certificate)
|
20
|
+
|
21
|
+
smime = Base64Helper.ensure_body_base64(decrypted_message)
|
22
|
+
message = read_smime(smime)
|
23
|
+
message.verify [partner_certificate], store
|
24
|
+
end
|
25
|
+
|
26
|
+
# Return the attached file, use .filename and .body on the return value
|
27
|
+
def attachment
|
28
|
+
if mail.has_attachments?
|
29
|
+
mail.attachments.find{|a| a.content_type == "application/edi-consent"}
|
30
|
+
else
|
31
|
+
mail
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
def mail
|
37
|
+
@mail ||= Mail.new(decrypted_message)
|
38
|
+
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
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
module As2
|
2
|
+
class MimeGenerator
|
3
|
+
class Part
|
4
|
+
def initialize
|
5
|
+
@parts = []
|
6
|
+
@body = ""
|
7
|
+
@headers = {}
|
8
|
+
end
|
9
|
+
|
10
|
+
def [](name)
|
11
|
+
@headers[name]
|
12
|
+
end
|
13
|
+
|
14
|
+
def []=(name, value)
|
15
|
+
@headers[name] = value
|
16
|
+
end
|
17
|
+
|
18
|
+
def body
|
19
|
+
@body
|
20
|
+
end
|
21
|
+
|
22
|
+
def body=(body)
|
23
|
+
unless @parts.empty?
|
24
|
+
raise "Cannot add plain budy to multipart"
|
25
|
+
end
|
26
|
+
@body = body
|
27
|
+
end
|
28
|
+
|
29
|
+
def add_part(part)
|
30
|
+
gen_id unless @id
|
31
|
+
@parts << part
|
32
|
+
@body = nil
|
33
|
+
end
|
34
|
+
|
35
|
+
def multipart?
|
36
|
+
! @parts.empty?
|
37
|
+
end
|
38
|
+
|
39
|
+
def write(io)
|
40
|
+
@headers.each do |name, value|
|
41
|
+
if multipart? && name =~ /content-type/i
|
42
|
+
io.print "#{name}: #{value}; \r\n"
|
43
|
+
io.print "\tboundary=\"----=_Part_#{@id}\"\r\n"
|
44
|
+
else
|
45
|
+
io.print "#{name}: #{value}\r\n"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
io.print "\r\n"
|
49
|
+
if @parts.empty?
|
50
|
+
io.print @body, "\r\n"
|
51
|
+
else
|
52
|
+
@parts.each do|p|
|
53
|
+
io.print "------=_Part_#{@id}\r\n"
|
54
|
+
p.write(io)
|
55
|
+
end
|
56
|
+
io.print "------=_Part_#{@id}--\r\n"
|
57
|
+
end
|
58
|
+
io.print "\r\n"
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
@@counter = 0
|
64
|
+
def gen_id
|
65
|
+
@@counter += 1
|
66
|
+
@id = "#{@@counter}_#{Time.now.strftime('%Y%m%d%H%M%S%L')}"
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
data/lib/as2/server.rb
ADDED
@@ -0,0 +1,139 @@
|
|
1
|
+
require 'rack'
|
2
|
+
require 'logger'
|
3
|
+
require 'stringio'
|
4
|
+
require 'as2/mime_generator'
|
5
|
+
require 'as2/base64_helper'
|
6
|
+
require 'as2/message'
|
7
|
+
|
8
|
+
module As2
|
9
|
+
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
|
+
attr_accessor :logger
|
20
|
+
|
21
|
+
def initialize(options = {}, &block)
|
22
|
+
@block = block
|
23
|
+
@info = Config.server_info
|
24
|
+
@options = options
|
25
|
+
end
|
26
|
+
|
27
|
+
def call(env)
|
28
|
+
if env['HTTP_AS2_TO'] != @info.name
|
29
|
+
return send_error(env, "Invalid destination name #{env['HTTP_AS2_TO']}")
|
30
|
+
end
|
31
|
+
|
32
|
+
partner = Config.partners[env['HTTP_AS2_FROM']]
|
33
|
+
unless partner
|
34
|
+
return send_error(env, "Invalid partner name #{env['HTTP_AS2_FROM']}")
|
35
|
+
end
|
36
|
+
|
37
|
+
smime_string = build_smime_text(env)
|
38
|
+
message = Message.new(smime_string, @info.pkey, @info.certificate)
|
39
|
+
unless message.valid_signature?(partner.certificate)
|
40
|
+
if @options[:on_signature_failure]
|
41
|
+
@options[:on_signature_failure].call({env: env, smime_string: smime_string})
|
42
|
+
else
|
43
|
+
raise "Could not verify signature"
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
mic = OpenSSL::Digest::SHA1.base64digest(message.decrypted_message)
|
48
|
+
|
49
|
+
if @block
|
50
|
+
begin
|
51
|
+
@block.call message.attachment.filename, message.attachment.body
|
52
|
+
rescue Exception => e
|
53
|
+
return send_error(env, e.message)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
send_mdn(env, mic)
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
def build_smime_text(env)
|
62
|
+
request = Rack::Request.new(env)
|
63
|
+
smime_data = StringIO.new
|
64
|
+
|
65
|
+
HEADER_MAP.each do |name, value|
|
66
|
+
smime_data.puts "#{name}: #{env[value]}"
|
67
|
+
end
|
68
|
+
|
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
|
+
report = MimeGenerator::Part.new
|
87
|
+
report['Content-Type'] = 'multipart/report; report-type=disposition-notification'
|
88
|
+
|
89
|
+
text = MimeGenerator::Part.new
|
90
|
+
text['Content-Type'] = 'text/plain'
|
91
|
+
text['Content-Transfer-Encoding'] = '7bit'
|
92
|
+
text.body = "The AS2 message has been received successfully"
|
93
|
+
|
94
|
+
report.add_part text
|
95
|
+
|
96
|
+
notification = MimeGenerator::Part.new
|
97
|
+
notification['Content-Type'] = 'message/disposition-notification'
|
98
|
+
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
|
+
notification.body = options.map{|n, v| "#{n}: #{v}"}.join("\r\n")
|
114
|
+
report.add_part notification
|
115
|
+
|
116
|
+
msg_out = StringIO.new
|
117
|
+
|
118
|
+
report.write msg_out
|
119
|
+
|
120
|
+
pkcs7 = OpenSSL::PKCS7.sign @info.certificate, @info.pkey, msg_out.string
|
121
|
+
pkcs7.detached = true
|
122
|
+
smime_signed = OpenSSL::PKCS7.write_smime pkcs7, msg_out.string
|
123
|
+
|
124
|
+
content_type = smime_signed[/^Content-Type: (.+?)$/m, 1]
|
125
|
+
smime_signed.sub!(/\A.+?^(?=---)/m, '')
|
126
|
+
|
127
|
+
headers = {}
|
128
|
+
headers['Content-Type'] = content_type
|
129
|
+
headers['MIME-Version'] = '1.0'
|
130
|
+
headers['Message-ID'] = "<#{@info.name}-#{Time.now.strftime('%Y%m%d%H%M%S')}@#{@info.domain}>"
|
131
|
+
headers['AS2-From'] = @info.name
|
132
|
+
headers['AS2-To'] = env['HTTP_AS2_FROM']
|
133
|
+
headers['AS2-Version'] = '1.2'
|
134
|
+
headers['Connection'] = 'close'
|
135
|
+
|
136
|
+
[200, headers, ["\r\n" + smime_signed]]
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
data/lib/as2/version.rb
ADDED
metadata
ADDED
@@ -0,0 +1,149 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: as2
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.2.4
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- OfficeLuv
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2019-03-12 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: mail
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rack
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: bundler
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '1.10'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '1.10'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rake
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '10.0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '10.0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: thin
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: minitest
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
description: Simple AS2 server and client implementation. Follows the AS2 implementation
|
98
|
+
from http://as2.mendelson-e-c.com
|
99
|
+
email:
|
100
|
+
- development@officeluv.com
|
101
|
+
executables: []
|
102
|
+
extensions: []
|
103
|
+
extra_rdoc_files: []
|
104
|
+
files:
|
105
|
+
- ".gitignore"
|
106
|
+
- Gemfile
|
107
|
+
- Gemfile.lock
|
108
|
+
- LICENSE.txt
|
109
|
+
- README.md
|
110
|
+
- Rakefile
|
111
|
+
- as2.gemspec
|
112
|
+
- bin/console
|
113
|
+
- bin/setup
|
114
|
+
- examples/client.rb
|
115
|
+
- examples/server.rb
|
116
|
+
- lib/as2.rb
|
117
|
+
- lib/as2/base64_helper.rb
|
118
|
+
- lib/as2/client.rb
|
119
|
+
- lib/as2/config.rb
|
120
|
+
- lib/as2/message.rb
|
121
|
+
- lib/as2/mime_generator.rb
|
122
|
+
- lib/as2/server.rb
|
123
|
+
- lib/as2/version.rb
|
124
|
+
homepage: https://github.com/officeluv/as2
|
125
|
+
licenses:
|
126
|
+
- MIT
|
127
|
+
metadata:
|
128
|
+
allowed_push_host: https://rubygems.org/
|
129
|
+
post_install_message:
|
130
|
+
rdoc_options: []
|
131
|
+
require_paths:
|
132
|
+
- lib
|
133
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
134
|
+
requirements:
|
135
|
+
- - ">="
|
136
|
+
- !ruby/object:Gem::Version
|
137
|
+
version: '0'
|
138
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
139
|
+
requirements:
|
140
|
+
- - ">="
|
141
|
+
- !ruby/object:Gem::Version
|
142
|
+
version: '0'
|
143
|
+
requirements: []
|
144
|
+
rubyforge_project:
|
145
|
+
rubygems_version: 2.7.6
|
146
|
+
signing_key:
|
147
|
+
specification_version: 4
|
148
|
+
summary: Simple AS2 server and client implementation
|
149
|
+
test_files: []
|