midi-smtp-server 2.3.3 → 3.0.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +112 -0
- data/MIT-LICENSE.txt +1 -1
- data/README.md +35 -950
- data/lib/midi-smtp-server/logger.rb +37 -0
- data/lib/midi-smtp-server/tls-transport.rb +54 -33
- data/lib/midi-smtp-server/version.rb +2 -2
- data/lib/midi-smtp-server.rb +178 -121
- metadata +5 -3
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'logger'
|
4
|
+
|
5
|
+
# A small and highly customizable ruby SMTP-Server.
|
6
|
+
module MidiSmtpServer
|
7
|
+
|
8
|
+
# class for Logging and support of on_logging_event
|
9
|
+
class ForwardingLogger
|
10
|
+
|
11
|
+
def initialize(on_logging_event)
|
12
|
+
@on_logging_event = on_logging_event
|
13
|
+
end
|
14
|
+
|
15
|
+
def info(msg)
|
16
|
+
@on_logging_event.call(nil, Logger::INFO, msg)
|
17
|
+
end
|
18
|
+
|
19
|
+
def warn(msg)
|
20
|
+
@on_logging_event.call(nil, Logger::WARN, msg)
|
21
|
+
end
|
22
|
+
|
23
|
+
def error(msg)
|
24
|
+
@on_logging_event.call(nil, Logger::ERROR, msg)
|
25
|
+
end
|
26
|
+
|
27
|
+
def fatal(msg)
|
28
|
+
@on_logging_event.call(nil, Logger::FATAL, msg)
|
29
|
+
end
|
30
|
+
|
31
|
+
def debug(msg)
|
32
|
+
@on_logging_event.call(nil, Logger::DEBUG, msg)
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
@@ -12,16 +12,19 @@ module MidiSmtpServer
|
|
12
12
|
# Encryption ciphers and methods
|
13
13
|
# check https://www.owasp.org/index.php/TLS_Cipher_String_Cheat_Sheet
|
14
14
|
TLS_CIPHERS_ADVANCED_PLUS = 'DHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256'
|
15
|
-
TLS_CIPHERS_ADVANCED =
|
16
|
-
TLS_CIPHERS_BROAD_COMP =
|
17
|
-
TLS_CIPHERS_WIDEST_COMP =
|
18
|
-
TLS_CIPHERS_LEGACY =
|
15
|
+
TLS_CIPHERS_ADVANCED = "#{TLS_CIPHERS_ADVANCED_PLUS}:DHE-RSA-AES256-SHA256:DHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256"
|
16
|
+
TLS_CIPHERS_BROAD_COMP = "#{TLS_CIPHERS_ADVANCED}:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA"
|
17
|
+
TLS_CIPHERS_WIDEST_COMP = "#{TLS_CIPHERS_ADVANCED}:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-SHA256:AES128-SHA256:AES256-SHA:AES128-SHA:DHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA"
|
18
|
+
TLS_CIPHERS_LEGACY = "#{TLS_CIPHERS_ADVANCED}:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-SHA256:AES128-SHA256:AES256-SHA:AES128-SHA:DES-CBC3-SHA:DHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA"
|
19
19
|
TLS_METHODS_ADVANCED = 'TLSv1_2'
|
20
20
|
TLS_METHODS_LEGACY = 'TLSv1_1'
|
21
21
|
|
22
22
|
# class for TlsTransport
|
23
23
|
class TlsTransport
|
24
24
|
|
25
|
+
# current TLS OpenSSL::SSL::SSLContext
|
26
|
+
attr_reader :ssl_context
|
27
|
+
|
25
28
|
def initialize(cert_path, key_path, ciphers, methods, cert_cn, cert_san, logger)
|
26
29
|
# if need to debug something while working with openssl
|
27
30
|
# OpenSSL::debug = true
|
@@ -31,18 +34,11 @@ module MidiSmtpServer
|
|
31
34
|
@cert_path = cert_path.to_s == '' ? nil : cert_path.strip
|
32
35
|
@key_path = key_path.to_s == '' ? nil : key_path.strip
|
33
36
|
# create SSL context
|
34
|
-
@
|
35
|
-
@
|
36
|
-
@
|
37
|
+
@ssl_context = OpenSSL::SSL::SSLContext.new
|
38
|
+
@ssl_context.ciphers = ciphers.to_s == '' ? TLS_CIPHERS_ADVANCED_PLUS : ciphers
|
39
|
+
@ssl_context.ssl_version = methods.to_s == '' ? TLS_METHODS_ADVANCED : methods
|
37
40
|
# check cert_path and key_path
|
38
|
-
if
|
39
|
-
# if any is set, test the pathes
|
40
|
-
raise "File \”#{@cert_path}\" does not exist or is not a regular file. Could not load certificate." unless File.file?(@cert_path.to_s)
|
41
|
-
raise "File \”#{@key_path}\" does not exist or is not a regular file. Could not load private key." unless File.file?(@key_path.to_s)
|
42
|
-
# try to load certificate and key
|
43
|
-
@ctx.cert = OpenSSL::X509::Certificate.new(File.open(@cert_path.to_s))
|
44
|
-
@ctx.key = OpenSSL::PKey::RSA.new(File.open(@key_path.to_s))
|
45
|
-
else
|
41
|
+
if @cert_path.nil?
|
46
42
|
# if none cert_path was set, create a self signed test certificate
|
47
43
|
# and try to setup common subject and subject alt name(s) for cert
|
48
44
|
@cert_cn = cert_cn.to_s.strip
|
@@ -52,33 +48,58 @@ module MidiSmtpServer
|
|
52
48
|
@cert_san.each { |san| @cert_san_ip << san if san =~ Resolv::IPv4::Regex || san =~ Resolv::IPv6::Regex }
|
53
49
|
# initialize self certificate and key
|
54
50
|
logger.debug("SSL: using self generated test certificate! CN=#{@cert_cn} SAN=[#{@cert_san.join(',')}]")
|
55
|
-
@
|
56
|
-
@
|
57
|
-
@
|
58
|
-
@
|
51
|
+
@ssl_context.key = OpenSSL::PKey::RSA.new 4096
|
52
|
+
@ssl_context.cert = OpenSSL::X509::Certificate.new
|
53
|
+
@ssl_context.cert.version = 2
|
54
|
+
@ssl_context.cert.serial = 1
|
59
55
|
# the subject and the issuer are identical only for test certificate
|
60
|
-
@
|
61
|
-
@
|
62
|
-
@
|
56
|
+
@ssl_context.cert.subject = OpenSSL::X509::Name.new [['CN', @cert_cn]]
|
57
|
+
@ssl_context.cert.issuer = @ssl_context.cert.subject
|
58
|
+
@ssl_context.cert.public_key = @ssl_context.key
|
63
59
|
# valid for 90 days
|
64
|
-
@
|
65
|
-
@
|
60
|
+
@ssl_context.cert.not_before = Time.now
|
61
|
+
@ssl_context.cert.not_after = Time.now + (60 * 60 * 24 * 90)
|
66
62
|
# setup some cert extensions
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
@
|
71
|
-
@
|
72
|
-
@
|
73
|
-
@
|
74
|
-
logger.debug("SSL: generated test certificate\r\n#{@
|
63
|
+
x509_extension_factory = OpenSSL::X509::ExtensionFactory.new
|
64
|
+
x509_extension_factory.subject_certificate = @ssl_context.cert
|
65
|
+
x509_extension_factory.issuer_certificate = @ssl_context.cert
|
66
|
+
@ssl_context.cert.add_extension(x509_extension_factory.create_extension('basicConstraints', 'CA:FALSE', false))
|
67
|
+
@ssl_context.cert.add_extension(x509_extension_factory.create_extension('keyUsage', 'digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment', false))
|
68
|
+
@ssl_context.cert.add_extension(x509_extension_factory.create_extension('subjectAltName', (@cert_san.map { |san| "DNS:#{san}" } + @cert_san_ip.map { |ip| "IP:#{ip}" }).join(', '), false))
|
69
|
+
@ssl_context.cert.sign @ssl_context.key, OpenSSL::Digest.new('SHA256')
|
70
|
+
logger.debug("SSL: generated test certificate\r\n#{@ssl_context.cert.to_text}")
|
71
|
+
else
|
72
|
+
# if any is set, test the pathes
|
73
|
+
raise "File \”#{@cert_path}\" does not exist or is not a regular file. Could not load certificate." unless File.file?(@cert_path.to_s)
|
74
|
+
raise "File \”#{@key_path}\" does not exist or is not a regular file. Could not load private key." unless @key_path.nil? || File.file?(@key_path.to_s)
|
75
|
+
# try to load certificate and key
|
76
|
+
cert_lines = File.read(@cert_path.to_s).lines
|
77
|
+
# check if the cert file contains a chain of certs
|
78
|
+
cert_indexes = cert_lines.each_with_index.map { |line, index| index if line.downcase.include?('-begin certificate-') }.compact
|
79
|
+
# create each cert in the chain
|
80
|
+
certs = []
|
81
|
+
cert_indexes.each_with_index do |cert_index, current_index|
|
82
|
+
end_index = current_index + 1 < cert_indexes.length ? cert_indexes[current_index + 1] : -1
|
83
|
+
certs << OpenSSL::X509::Certificate.new(cert_lines[cert_index..end_index].join)
|
84
|
+
end
|
85
|
+
# add the cert and optional found chain to context
|
86
|
+
@ssl_context.cert = certs.first
|
87
|
+
@ssl_context.extra_chain_cert = certs[1..]
|
88
|
+
# check if key was given by separate file or should be included in cert
|
89
|
+
if @key_path.nil?
|
90
|
+
key_index = cert_lines.index { |line| line =~ /-begin[^-]+key-/i }
|
91
|
+
end_index = cert_lines.index { |line| line =~ /-end[^-]+key-/i }
|
92
|
+
@ssl_context.key = OpenSSL::PKey::RSA.new(cert_lines[key_index..end_index].join)
|
93
|
+
else
|
94
|
+
@ssl_context.key = OpenSSL::PKey::RSA.new(File.open(@key_path.to_s))
|
95
|
+
end
|
75
96
|
end
|
76
97
|
end
|
77
98
|
|
78
99
|
# start ssl connection over existing tcpserver socket
|
79
100
|
def start(io)
|
80
101
|
# start SSL negotiation
|
81
|
-
ssl = OpenSSL::SSL::SSLSocket.new(io, @
|
102
|
+
ssl = OpenSSL::SSL::SSLSocket.new(io, @ssl_context)
|
82
103
|
# connect to server socket
|
83
104
|
ssl.accept
|
84
105
|
# make sure to close also the underlying io
|