remailer 0.2.1 → 0.3.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.
- data/VERSION +1 -1
- data/lib/remailer.rb +1 -0
- data/lib/remailer/connection.rb +132 -377
- data/lib/remailer/connection/smtp_interpreter.rb +270 -0
- data/lib/remailer/connection/socks5_interpreter.rb +186 -0
- data/lib/remailer/interpreter.rb +253 -0
- data/lib/remailer/interpreter/state_proxy.rb +43 -0
- data/remailer.gemspec +19 -3
- data/test/config.example.rb +17 -0
- data/test/helper.rb +61 -2
- data/test/unit/remailer_connection_smtp_interpreter_test.rb +347 -0
- data/test/unit/remailer_connection_socks5_interpreter_test.rb +116 -0
- data/test/unit/remailer_connection_test.rb +287 -0
- data/test/unit/remailer_interpreter_state_proxy_test.rb +86 -0
- data/test/unit/remailer_interpreter_test.rb +153 -0
- data/test/unit/remailer_test.rb +2 -223
- metadata +20 -4
@@ -0,0 +1,270 @@
|
|
1
|
+
class Remailer::Connection::SmtpInterpreter < Remailer::Interpreter
|
2
|
+
# == Constants ============================================================
|
3
|
+
|
4
|
+
LINE_REGEXP = /^.*?\r?\n/.freeze
|
5
|
+
|
6
|
+
# == Properties ===========================================================
|
7
|
+
|
8
|
+
# == Class Methods ========================================================
|
9
|
+
|
10
|
+
# Expands a standard SMTP reply into three parts: Numerical code, message
|
11
|
+
# and a boolean indicating if this reply is continued on a subsequent line.
|
12
|
+
def self.split_reply(reply)
|
13
|
+
reply.match(/(\d+)([ \-])(.*)/) and [ $1.to_i, $3, $2 == '-' ? :continued : nil ].compact
|
14
|
+
end
|
15
|
+
|
16
|
+
# Encodes the given user authentication paramters as a Base64-encoded
|
17
|
+
# string as defined by RFC4954
|
18
|
+
def self.encode_authentication(username, password)
|
19
|
+
base64("\0#{username}\0#{password}")
|
20
|
+
end
|
21
|
+
|
22
|
+
# Encodes the given data for an RFC5321-compliant stream where lines with
|
23
|
+
# leading period chracters are escaped.
|
24
|
+
def self.encode_data(data)
|
25
|
+
data.gsub(/((?:\r\n|\n)\.)/m, '\\1.')
|
26
|
+
end
|
27
|
+
|
28
|
+
# Encodes a string in Base64 as a single line
|
29
|
+
def self.base64(string)
|
30
|
+
[ string.to_s ].pack('m').chomp
|
31
|
+
end
|
32
|
+
|
33
|
+
# == State Mapping ========================================================
|
34
|
+
|
35
|
+
parse(LINE_REGEXP) do |data|
|
36
|
+
split_reply(data.chomp)
|
37
|
+
end
|
38
|
+
|
39
|
+
state :initialized do
|
40
|
+
interpret(220) do |message|
|
41
|
+
message_parts = message.split(/\s+/)
|
42
|
+
delegate.remote = message_parts.first
|
43
|
+
|
44
|
+
if (message_parts.include?('ESMTP'))
|
45
|
+
delegate.protocol = :esmtp
|
46
|
+
enter_state(:ehlo)
|
47
|
+
else
|
48
|
+
delegate.protocol = :smtp
|
49
|
+
enter_state(:helo)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
state :helo do
|
55
|
+
enter do
|
56
|
+
delegate.send_line("HELO #{delegate.hostname}")
|
57
|
+
end
|
58
|
+
|
59
|
+
interpret(250) do
|
60
|
+
enter_state(:established)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
state :ehlo do
|
65
|
+
enter do
|
66
|
+
delegate.send_line("EHLO #{delegate.hostname}")
|
67
|
+
end
|
68
|
+
|
69
|
+
interpret(250) do |message, continues|
|
70
|
+
message_parts = message.split(/\s+/)
|
71
|
+
|
72
|
+
case (message_parts[0].to_s.upcase)
|
73
|
+
when 'SIZE'
|
74
|
+
delegate.max_size = message_parts[1].to_i
|
75
|
+
when 'PIPELINING'
|
76
|
+
delegate.pipelining = true
|
77
|
+
when 'STARTTLS'
|
78
|
+
delegate.tls_support = true
|
79
|
+
when 'AUTH'
|
80
|
+
delegate.auth_support = message_parts[1, message_parts.length].inject({ }) do |h, v|
|
81
|
+
h[v] = true
|
82
|
+
h
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
unless (continues)
|
87
|
+
if (delegate.use_tls? and delegate.tls_support?)
|
88
|
+
enter_state(:starttls)
|
89
|
+
elsif (delegate.requires_authentication?)
|
90
|
+
enter_state(:auth)
|
91
|
+
else
|
92
|
+
enter_state(:established)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
state :starttls do
|
99
|
+
enter do
|
100
|
+
delegate.send_line("STARTTLS")
|
101
|
+
end
|
102
|
+
|
103
|
+
interpret(220) do
|
104
|
+
delegate.start_tls
|
105
|
+
|
106
|
+
if (delegate.requires_authentication?)
|
107
|
+
enter_state(:auth)
|
108
|
+
else
|
109
|
+
enter_state(:established)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
state :auth do
|
115
|
+
enter do
|
116
|
+
delegate.send_line("AUTH PLAIN #{self.class.encode_authentication(delegate.options[:username], delegate.options[:password])}")
|
117
|
+
end
|
118
|
+
|
119
|
+
interpret(235) do
|
120
|
+
enter_state(:established)
|
121
|
+
end
|
122
|
+
|
123
|
+
interpret(535) do |message, continues|
|
124
|
+
if (@error)
|
125
|
+
@error << ' '
|
126
|
+
|
127
|
+
if (message.match(/^(\S+)/).to_s == @error.match(/^(\S+)/).to_s)
|
128
|
+
@error << message.sub(/^\S+/, '')
|
129
|
+
else
|
130
|
+
@error << message
|
131
|
+
end
|
132
|
+
else
|
133
|
+
@error = message
|
134
|
+
end
|
135
|
+
|
136
|
+
unless (continues)
|
137
|
+
enter_state(:quit)
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
state :established do
|
143
|
+
enter do
|
144
|
+
delegate.connect_notification(true)
|
145
|
+
|
146
|
+
enter_state(:ready)
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
state :ready do
|
151
|
+
enter do
|
152
|
+
delegate.after_ready
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
state :send do
|
157
|
+
enter do
|
158
|
+
enter_state(:mail_from)
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
state :mail_from do
|
163
|
+
enter do
|
164
|
+
delegate.send_line("MAIL FROM:#{delegate.active_message[:from]}")
|
165
|
+
end
|
166
|
+
|
167
|
+
interpret(250) do
|
168
|
+
enter_state(:rcpt_to)
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
state :rcpt_to do
|
173
|
+
enter do
|
174
|
+
delegate.send_line("RCPT TO:#{delegate.active_message[:to]}")
|
175
|
+
end
|
176
|
+
|
177
|
+
interpret(250) do
|
178
|
+
enter_state(:data)
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
state :data do
|
183
|
+
enter do
|
184
|
+
delegate.send_line("DATA")
|
185
|
+
end
|
186
|
+
|
187
|
+
interpret(354) do
|
188
|
+
enter_state(:sending)
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
state :sending do
|
193
|
+
enter do
|
194
|
+
data = delegate.active_message[:data]
|
195
|
+
|
196
|
+
delegate.debug_notification(:send, data.inspect)
|
197
|
+
|
198
|
+
delegate.send_data(self.class.encode_data(data))
|
199
|
+
|
200
|
+
# Ensure that a blank line is sent after the last bit of email content
|
201
|
+
# to ensure that the dot is on its own line.
|
202
|
+
delegate.send_line
|
203
|
+
delegate.send_line(".")
|
204
|
+
end
|
205
|
+
|
206
|
+
default do |reply_code, reply_message|
|
207
|
+
delegate_call(:after_message_sent, reply_code, reply_message)
|
208
|
+
|
209
|
+
enter_state(:sent)
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
state :sent do
|
214
|
+
enter do
|
215
|
+
enter_state(:ready)
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
state :quit do
|
220
|
+
enter do
|
221
|
+
delegate.send_line("QUIT")
|
222
|
+
end
|
223
|
+
|
224
|
+
interpret(221) do
|
225
|
+
enter_state(:terminated)
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
state :terminated do
|
230
|
+
enter do
|
231
|
+
delegate.close_connection
|
232
|
+
end
|
233
|
+
end
|
234
|
+
|
235
|
+
state :reset do
|
236
|
+
enter do
|
237
|
+
delegate.send_line("RESET")
|
238
|
+
end
|
239
|
+
|
240
|
+
interpret(250) do
|
241
|
+
enter_state(:ready)
|
242
|
+
end
|
243
|
+
end
|
244
|
+
|
245
|
+
state :noop do
|
246
|
+
enter do
|
247
|
+
delegate.send_line("NOOP")
|
248
|
+
end
|
249
|
+
|
250
|
+
interpret(250) do
|
251
|
+
enter_state(:ready)
|
252
|
+
end
|
253
|
+
end
|
254
|
+
|
255
|
+
on_error do |reply_code, reply_message|
|
256
|
+
delegate.send_callback(reply_code, reply_message)
|
257
|
+
delegate.debug_notification(:error, "[#{@state}] #{reply_code} #{reply_message}")
|
258
|
+
delegate.error_notification(reply_code, reply_message)
|
259
|
+
|
260
|
+
delegate.active_message = nil
|
261
|
+
|
262
|
+
enter_state(delegate.protocol ? :reset : :terminated)
|
263
|
+
end
|
264
|
+
|
265
|
+
# == Instance Methods =====================================================
|
266
|
+
|
267
|
+
def label
|
268
|
+
'SMTP'
|
269
|
+
end
|
270
|
+
end
|
@@ -0,0 +1,186 @@
|
|
1
|
+
class Remailer::Connection::Socks5Interpreter < Remailer::Interpreter
|
2
|
+
# == Constants ============================================================
|
3
|
+
|
4
|
+
SOCKS5_VERSION = 5
|
5
|
+
|
6
|
+
SOCKS5_METHOD = {
|
7
|
+
:no_auth => 0,
|
8
|
+
:gssapi => 1,
|
9
|
+
:username_password => 2
|
10
|
+
}.freeze
|
11
|
+
|
12
|
+
SOCKS5_COMMAND = {
|
13
|
+
:connect => 1,
|
14
|
+
:bind => 2
|
15
|
+
}.freeze
|
16
|
+
|
17
|
+
SOCKS5_REPLY = {
|
18
|
+
0 => 'Succeeded',
|
19
|
+
1 => 'General SOCKS server failure',
|
20
|
+
2 => 'Connection not allowed',
|
21
|
+
3 => 'Network unreachable',
|
22
|
+
4 => 'Host unreachable',
|
23
|
+
5 => 'Connection refused',
|
24
|
+
6 => 'TTL expired',
|
25
|
+
7 => 'Command not supported',
|
26
|
+
8 => 'Address type not supported'
|
27
|
+
}.freeze
|
28
|
+
|
29
|
+
SOCKS5_ADDRESS_TYPE = {
|
30
|
+
:ipv4 => 1,
|
31
|
+
:domainname => 3,
|
32
|
+
:ipv6 => 4
|
33
|
+
}.freeze
|
34
|
+
|
35
|
+
# == State Mapping ========================================================
|
36
|
+
|
37
|
+
state :initialized do
|
38
|
+
enter do
|
39
|
+
enter_state(:connect_to_proxy)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
state :connect_to_proxy do
|
44
|
+
enter do
|
45
|
+
proxy_options = delegate.options[:proxy]
|
46
|
+
|
47
|
+
delegate.debug_notification(:proxy, "Initiating proxy connection through #{proxy_options[:host]}")
|
48
|
+
|
49
|
+
socks_methods = [ ]
|
50
|
+
|
51
|
+
if (proxy_options[:username])
|
52
|
+
socks_methods << SOCKS5_METHOD[:username_password]
|
53
|
+
end
|
54
|
+
|
55
|
+
delegate.send_data(
|
56
|
+
[
|
57
|
+
SOCKS5_VERSION,
|
58
|
+
socks_methods.length,
|
59
|
+
socks_methods
|
60
|
+
].flatten.pack('CCC*')
|
61
|
+
)
|
62
|
+
end
|
63
|
+
|
64
|
+
parse do |s|
|
65
|
+
return unless (s.length >= 2)
|
66
|
+
|
67
|
+
version, method = s.slice!(0,2).unpack('CC')
|
68
|
+
|
69
|
+
method
|
70
|
+
end
|
71
|
+
|
72
|
+
interpret(SOCKS5_METHOD[:username_password]) do
|
73
|
+
enter_state(:authentication)
|
74
|
+
end
|
75
|
+
|
76
|
+
default do
|
77
|
+
enter_state(:resolving_destination)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
state :resolving_destination do
|
82
|
+
enter do
|
83
|
+
# FIX: Use an async resolver here
|
84
|
+
@destination_address = delegate.resolve_hostname(delegate.options[:host])
|
85
|
+
enter_state(:connect_through_proxy)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
state :connect_through_proxy do
|
90
|
+
enter do
|
91
|
+
delegate.debug_notification(:proxy, "Sending proxy connection request to #{delegate.options[:host]}:#{delegate.options[:port]}")
|
92
|
+
|
93
|
+
if (@destination_address)
|
94
|
+
delegate.send_data(
|
95
|
+
[
|
96
|
+
SOCKS5_VERSION,
|
97
|
+
SOCKS5_COMMAND[:connect],
|
98
|
+
0,
|
99
|
+
SOCKS5_ADDRESS_TYPE[:ipv4],
|
100
|
+
@destination_address,
|
101
|
+
delegate.options[:port]
|
102
|
+
].pack('CCCCA4n')
|
103
|
+
)
|
104
|
+
else
|
105
|
+
delegate.send_callback(:error_connecting, "Could not resolve hostname #{delegate.options[:host]}")
|
106
|
+
enter_state(:failed)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
parse do |s|
|
111
|
+
return unless (s.length >= 10)
|
112
|
+
|
113
|
+
version, reply, reserved, address_type, address, port = s.slice!(0,10).unpack('CCCCNn')
|
114
|
+
|
115
|
+
[
|
116
|
+
reply,
|
117
|
+
{
|
118
|
+
:address => address,
|
119
|
+
:port => port,
|
120
|
+
:address_type => address_type
|
121
|
+
}
|
122
|
+
]
|
123
|
+
end
|
124
|
+
|
125
|
+
interpret(0) do
|
126
|
+
enter_state(:connected)
|
127
|
+
end
|
128
|
+
|
129
|
+
default do |reply|
|
130
|
+
@reply = reply
|
131
|
+
enter_state(:failed)
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
state :authentication do
|
136
|
+
enter do
|
137
|
+
delegate.debug_notification(:proxy, "Sending proxy authentication")
|
138
|
+
|
139
|
+
proxy_options = delegate.options[:proxy]
|
140
|
+
username = proxy_options[:username]
|
141
|
+
password = proxy_options[:password]
|
142
|
+
|
143
|
+
send_data(
|
144
|
+
[
|
145
|
+
SOCKS5_VERSION,
|
146
|
+
username.length,
|
147
|
+
username,
|
148
|
+
password.length,
|
149
|
+
password
|
150
|
+
].pack('CCA*CA*')
|
151
|
+
)
|
152
|
+
end
|
153
|
+
|
154
|
+
parse do |s|
|
155
|
+
end
|
156
|
+
|
157
|
+
interpret(0) do
|
158
|
+
enter_state(:connected)
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
state :connected do
|
163
|
+
enter do
|
164
|
+
delegate_call(:after_proxy_connected)
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
state :failed do
|
169
|
+
enter do
|
170
|
+
message = "Proxy server returned error code #{@reply}: #{SOCKS5_REPLY[@reply]}"
|
171
|
+
delegate.debug(:error, message)
|
172
|
+
delegate.connect_notification(false, message)
|
173
|
+
delegate.close_connection
|
174
|
+
end
|
175
|
+
|
176
|
+
terminate
|
177
|
+
end
|
178
|
+
|
179
|
+
# == Class Methods ========================================================
|
180
|
+
|
181
|
+
# == Instance Methods =====================================================
|
182
|
+
|
183
|
+
def label
|
184
|
+
'SOCKS5'
|
185
|
+
end
|
186
|
+
end
|