ftpd 0.0.1.pre → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of ftpd might be problematic. Click here for more details.
- data/Gemfile +3 -1
- data/Gemfile.lock +14 -14
- data/README.md +71 -23
- data/Rakefile +9 -3
- data/VERSION +1 -1
- data/examples/example.rb +132 -53
- data/examples/hello_world.rb +32 -0
- data/features/example/example.feature +18 -0
- data/features/example/step_definitions/example_server.rb +3 -0
- data/features/{command_errors.feature → ftp_server/command_errors.feature} +3 -0
- data/features/ftp_server/concurrent_sessions.feature +14 -0
- data/features/ftp_server/debug.feature +15 -0
- data/features/{delete.feature → ftp_server/delete.feature} +23 -2
- data/features/{directory_navigation.feature → ftp_server/directory_navigation.feature} +17 -4
- data/features/ftp_server/file_structure.feature +43 -0
- data/features/{get.feature → ftp_server/get.feature} +21 -10
- data/features/ftp_server/get_tls.feature +18 -0
- data/features/{list.feature → ftp_server/list.feature} +24 -30
- data/features/ftp_server/list_tls.feature +21 -0
- data/features/{login.feature → ftp_server/login.feature} +4 -2
- data/features/ftp_server/mode.feature +43 -0
- data/features/{name_list.feature → ftp_server/name_list.feature} +25 -31
- data/features/ftp_server/name_list_tls.feature +22 -0
- data/features/{noop.feature → ftp_server/noop.feature} +3 -0
- data/features/{port.feature → ftp_server/port.feature} +3 -0
- data/features/{put.feature → ftp_server/put.feature} +19 -11
- data/features/ftp_server/put_tls.feature +18 -0
- data/features/{quit.feature → ftp_server/quit.feature} +3 -0
- data/features/ftp_server/step_definitions/debug.rb +8 -0
- data/features/ftp_server/step_definitions/test_server.rb +12 -0
- data/features/{syntax_errors.feature → ftp_server/syntax_errors.feature} +3 -0
- data/features/ftp_server/type.feature +56 -0
- data/features/step_definitions/error.rb +10 -3
- data/features/step_definitions/list.rb +21 -4
- data/features/step_definitions/login.rb +0 -2
- data/features/step_definitions/server_files.rb +4 -0
- data/features/step_definitions/stop_server.rb +3 -0
- data/features/support/example_server.rb +58 -0
- data/features/support/test_client.rb +1 -1
- data/features/support/test_server.rb +106 -24
- data/features/support/test_server_files.rb +30 -0
- data/ftpd.gemspec +56 -25
- data/lib/ftpd.rb +22 -4
- data/lib/ftpd/disk_file_system.rb +137 -0
- data/lib/ftpd/error.rb +9 -0
- data/lib/ftpd/exception_translator.rb +29 -0
- data/lib/ftpd/exceptions.rb +13 -0
- data/lib/ftpd/file_system_error_translator.rb +21 -0
- data/lib/ftpd/ftp_server.rb +8 -645
- data/lib/ftpd/insecure_certificate.rb +10 -0
- data/lib/ftpd/server.rb +15 -12
- data/lib/ftpd/session.rb +569 -0
- data/lib/ftpd/temp_dir.rb +10 -11
- data/lib/ftpd/tls_server.rb +27 -15
- data/lib/ftpd/translate_exceptions.rb +44 -0
- data/rake_tasks/cucumber.rake +4 -2
- data/rake_tasks/default.rake +1 -0
- data/rake_tasks/spec.rake +3 -0
- data/rake_tasks/test.rake +2 -0
- data/sandbox/em-server.rb +37 -0
- data/spec/disk_file_system_spec.rb +239 -0
- data/spec/exception_translator_spec.rb +35 -0
- data/spec/spec_helper.rb +5 -0
- data/spec/translate_exceptions_spec.rb +40 -0
- metadata +143 -115
- data/features/concurrent_sessions.feature +0 -11
- data/features/file_structure.feature +0 -40
- data/features/mode.feature +0 -40
- data/features/step_definitions/server.rb +0 -7
- data/features/type.feature +0 -53
data/lib/ftpd/server.rb
CHANGED
@@ -1,30 +1,33 @@
|
|
1
|
-
require 'socket'
|
2
|
-
|
3
1
|
module Ftpd
|
4
2
|
class Server
|
5
3
|
|
4
|
+
include Memoizer
|
5
|
+
|
6
|
+
attr_accessor :interface
|
7
|
+
attr_accessor :port
|
8
|
+
|
6
9
|
def initialize
|
7
|
-
@
|
8
|
-
@
|
10
|
+
@interface = 'localhost'
|
11
|
+
@port = 0
|
9
12
|
end
|
10
13
|
|
11
|
-
def
|
14
|
+
def bound_port
|
12
15
|
@server_socket.addr[1]
|
13
16
|
end
|
14
17
|
|
15
|
-
def
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
18
|
+
def start
|
19
|
+
@server_socket = make_server_socket
|
20
|
+
@server_thread = make_server_thread
|
21
|
+
end
|
22
|
+
|
23
|
+
def stop
|
21
24
|
@server_socket.close
|
22
25
|
end
|
23
26
|
|
24
27
|
private
|
25
28
|
|
26
29
|
def make_server_socket
|
27
|
-
return TCPServer.new(
|
30
|
+
return TCPServer.new(@interface, @port)
|
28
31
|
end
|
29
32
|
|
30
33
|
def make_server_thread
|
data/lib/ftpd/session.rb
ADDED
@@ -0,0 +1,569 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
module Ftpd
|
4
|
+
class Session
|
5
|
+
|
6
|
+
include Error
|
7
|
+
|
8
|
+
def initialize(opts)
|
9
|
+
@driver = opts[:driver]
|
10
|
+
@socket = opts[:socket]
|
11
|
+
@tls = opts[:tls]
|
12
|
+
if @tls == :implicit
|
13
|
+
@socket.encrypt
|
14
|
+
end
|
15
|
+
@name_prefix = '/'
|
16
|
+
@debug_path = opts[:debug_path]
|
17
|
+
@debug = opts[:debug]
|
18
|
+
@data_type = 'A'
|
19
|
+
@mode = 'S'
|
20
|
+
@format = 'N'
|
21
|
+
@structure = 'F'
|
22
|
+
@response_delay = opts[:response_delay]
|
23
|
+
@data_channel_protection_level = :clear
|
24
|
+
end
|
25
|
+
|
26
|
+
def run
|
27
|
+
reply "220 FakeFtpServer"
|
28
|
+
@state = :user
|
29
|
+
catch :done do
|
30
|
+
loop do
|
31
|
+
begin
|
32
|
+
s = get_command
|
33
|
+
syntax_error unless s =~ /^(\w+)(?: (.*))?$/
|
34
|
+
command, argument = $1.downcase, $2
|
35
|
+
unless VALID_COMMANDS.include?(command)
|
36
|
+
error "500 Syntax error, command unrecognized: #{s}"
|
37
|
+
end
|
38
|
+
method = 'cmd_' + command
|
39
|
+
unless self.class.private_method_defined?(method)
|
40
|
+
error "502 Command not implemented: #{command}"
|
41
|
+
end
|
42
|
+
send(method, argument)
|
43
|
+
rescue CommandError => e
|
44
|
+
reply e.message
|
45
|
+
rescue Errno::ECONNRESET, Errno::EPIPE
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
VALID_COMMANDS = [
|
54
|
+
"abor",
|
55
|
+
"acct",
|
56
|
+
"allo",
|
57
|
+
"appe",
|
58
|
+
"auth",
|
59
|
+
"pbsz",
|
60
|
+
"cdup",
|
61
|
+
"cwd",
|
62
|
+
"dele",
|
63
|
+
"help",
|
64
|
+
"list",
|
65
|
+
"mkd",
|
66
|
+
"mode",
|
67
|
+
"nlst",
|
68
|
+
"noop",
|
69
|
+
"pass",
|
70
|
+
"pasv",
|
71
|
+
"port",
|
72
|
+
"prot",
|
73
|
+
"pwd",
|
74
|
+
"quit",
|
75
|
+
"rein",
|
76
|
+
"rest",
|
77
|
+
"retr",
|
78
|
+
"rmd",
|
79
|
+
"rnfr",
|
80
|
+
"rnto",
|
81
|
+
"site",
|
82
|
+
"smnt",
|
83
|
+
"stat",
|
84
|
+
"stor",
|
85
|
+
"stou",
|
86
|
+
"stru",
|
87
|
+
"syst",
|
88
|
+
"type",
|
89
|
+
"user",
|
90
|
+
]
|
91
|
+
|
92
|
+
def cmd_user(argument)
|
93
|
+
syntax_error unless argument
|
94
|
+
bad_sequence unless @state == :user
|
95
|
+
@user = argument
|
96
|
+
@state = :password
|
97
|
+
reply "331 Password required"
|
98
|
+
end
|
99
|
+
|
100
|
+
def bad_sequence
|
101
|
+
error "503 Bad sequence of commands"
|
102
|
+
end
|
103
|
+
|
104
|
+
def cmd_pass(argument)
|
105
|
+
syntax_error unless argument
|
106
|
+
bad_sequence unless @state == :password
|
107
|
+
password = argument
|
108
|
+
unless @driver.authenticate(@user, password)
|
109
|
+
@state = :user
|
110
|
+
error "530 Login incorrect"
|
111
|
+
end
|
112
|
+
reply "230 Logged in"
|
113
|
+
set_file_system @driver.file_system(@user)
|
114
|
+
@state = :logged_in
|
115
|
+
end
|
116
|
+
|
117
|
+
def cmd_quit(argument)
|
118
|
+
syntax_error if argument
|
119
|
+
ensure_logged_in
|
120
|
+
reply "221 Byebye"
|
121
|
+
@state = :user
|
122
|
+
end
|
123
|
+
|
124
|
+
def syntax_error
|
125
|
+
error "501 Syntax error"
|
126
|
+
end
|
127
|
+
|
128
|
+
def cmd_port(argument)
|
129
|
+
ensure_logged_in
|
130
|
+
pieces = argument.split(/,/)
|
131
|
+
syntax_error unless pieces.size == 6
|
132
|
+
pieces.collect! do |s|
|
133
|
+
syntax_error unless s =~ /^\d{1,3}$/
|
134
|
+
i = s.to_i
|
135
|
+
syntax_error unless (0..255) === i
|
136
|
+
i
|
137
|
+
end
|
138
|
+
@data_hostname = pieces[0..3].join('.')
|
139
|
+
@data_port = pieces[4] << 8 | pieces[5]
|
140
|
+
reply "200 PORT command successful"
|
141
|
+
end
|
142
|
+
|
143
|
+
def cmd_stor(argument)
|
144
|
+
close_data_server_socket_when_done do
|
145
|
+
ensure_logged_in
|
146
|
+
path = argument
|
147
|
+
syntax_error unless path
|
148
|
+
path = File.expand_path(path, @name_prefix)
|
149
|
+
ensure_accessible path
|
150
|
+
ensure_exists File.dirname(path)
|
151
|
+
contents = receive_file(path)
|
152
|
+
@file_system.write path, contents
|
153
|
+
reply "226 Transfer complete"
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
def cmd_retr(argument)
|
158
|
+
close_data_server_socket_when_done do
|
159
|
+
ensure_logged_in
|
160
|
+
path = argument
|
161
|
+
syntax_error unless path
|
162
|
+
path = File.expand_path(path, @name_prefix)
|
163
|
+
ensure_accessible path
|
164
|
+
ensure_exists path
|
165
|
+
contents = @file_system.read(path)
|
166
|
+
transmit_file contents
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
def cmd_dele(argument)
|
171
|
+
ensure_logged_in
|
172
|
+
path = argument
|
173
|
+
error "501 Path required" unless path
|
174
|
+
path = File.expand_path(path, @name_prefix)
|
175
|
+
ensure_accessible path
|
176
|
+
ensure_exists path
|
177
|
+
@file_system.delete path
|
178
|
+
reply "250 DELE command successful"
|
179
|
+
end
|
180
|
+
|
181
|
+
def cmd_list(argument)
|
182
|
+
ls(argument, :list_long)
|
183
|
+
end
|
184
|
+
|
185
|
+
def cmd_nlst(argument)
|
186
|
+
ls(argument, :list_short)
|
187
|
+
end
|
188
|
+
|
189
|
+
def ls(path, file_system_method)
|
190
|
+
close_data_server_socket_when_done do
|
191
|
+
ensure_logged_in
|
192
|
+
path ||= '.'
|
193
|
+
path = File.expand_path(path, @name_prefix)
|
194
|
+
list = @file_system.send(file_system_method, path)
|
195
|
+
transmit_file(list, 'A')
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
def realpath(pathname)
|
200
|
+
handle_system_error do
|
201
|
+
basename = File.basename(pathname.to_s)
|
202
|
+
if is_glob?(basename)
|
203
|
+
pathname.dirname.realpath + basename
|
204
|
+
else
|
205
|
+
pathname.realpath
|
206
|
+
end
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
def is_glob?(filename)
|
211
|
+
filename =~ /[.*]/
|
212
|
+
end
|
213
|
+
|
214
|
+
def cmd_type(argument)
|
215
|
+
ensure_logged_in
|
216
|
+
syntax_error unless argument =~ /^(\S)(?: (\S+))?$/
|
217
|
+
type_code = $1
|
218
|
+
format_code = $2
|
219
|
+
set_type(type_code)
|
220
|
+
set_format(format_code)
|
221
|
+
reply "200 Type set to #{@data_type}"
|
222
|
+
end
|
223
|
+
|
224
|
+
def set_type(type_code)
|
225
|
+
name, implemented = DATA_TYPES[type_code]
|
226
|
+
error "504 Invalid type code" unless name
|
227
|
+
error "504 Type not implemented" unless implemented
|
228
|
+
@data_type = type_code
|
229
|
+
end
|
230
|
+
|
231
|
+
def set_format(format_code)
|
232
|
+
format_code ||= 'N'
|
233
|
+
name, implemented = FORMAT_TYPES[format_code]
|
234
|
+
error "504 Invalid format code" unless name
|
235
|
+
error "504 Format not implemented" unless implemented
|
236
|
+
@data_format = format_code
|
237
|
+
end
|
238
|
+
|
239
|
+
def cmd_mode(argument)
|
240
|
+
syntax_error unless argument
|
241
|
+
ensure_logged_in
|
242
|
+
name, implemented = TRANSMISSION_MODES[argument]
|
243
|
+
error "504 Invalid mode code" unless name
|
244
|
+
error "504 Mode not implemented" unless implemented
|
245
|
+
@mode = argument
|
246
|
+
reply "200 Mode set to #{name}"
|
247
|
+
end
|
248
|
+
|
249
|
+
def cmd_stru(argument)
|
250
|
+
syntax_error unless argument
|
251
|
+
ensure_logged_in
|
252
|
+
name, implemented = FILE_STRUCTURES[argument]
|
253
|
+
error "504 Invalid structure code" unless name
|
254
|
+
error "504 Structure not implemented" unless implemented
|
255
|
+
@structure = argument
|
256
|
+
reply "200 File structure set to #{name}"
|
257
|
+
end
|
258
|
+
|
259
|
+
def cmd_noop(argument)
|
260
|
+
syntax_error if argument
|
261
|
+
reply "200 Nothing done"
|
262
|
+
end
|
263
|
+
|
264
|
+
def cmd_pasv(argument)
|
265
|
+
ensure_logged_in
|
266
|
+
if @data_server
|
267
|
+
reply "200 Already in passive mode"
|
268
|
+
else
|
269
|
+
@data_server = TCPServer.new('localhost', 0)
|
270
|
+
ip = @data_server.addr[3]
|
271
|
+
port = @data_server.addr[1]
|
272
|
+
quads = [
|
273
|
+
ip.scan(/\d+/),
|
274
|
+
port >> 8,
|
275
|
+
port & 0xff,
|
276
|
+
].flatten.join(',')
|
277
|
+
reply "227 Entering passive mode (#{quads})"
|
278
|
+
end
|
279
|
+
end
|
280
|
+
|
281
|
+
def cmd_cwd(argument)
|
282
|
+
ensure_logged_in
|
283
|
+
path = File.expand_path(argument, @name_prefix)
|
284
|
+
ensure_accessible path
|
285
|
+
ensure_exists path
|
286
|
+
ensure_directory path
|
287
|
+
@name_prefix = path
|
288
|
+
pwd
|
289
|
+
end
|
290
|
+
|
291
|
+
def ensure_logged_in
|
292
|
+
return if @state == :logged_in
|
293
|
+
error "530 Not logged in"
|
294
|
+
end
|
295
|
+
|
296
|
+
def ensure_accessible(path)
|
297
|
+
unless @file_system.accessible?(path)
|
298
|
+
error '550 Access denied'
|
299
|
+
end
|
300
|
+
end
|
301
|
+
|
302
|
+
def ensure_exists(path)
|
303
|
+
unless @file_system.exists?(path)
|
304
|
+
error '550 No such file or directory'
|
305
|
+
end
|
306
|
+
end
|
307
|
+
|
308
|
+
def ensure_directory(path)
|
309
|
+
unless @file_system.directory?(path)
|
310
|
+
error '550 Not a directory'
|
311
|
+
end
|
312
|
+
end
|
313
|
+
|
314
|
+
def ensure_tls_supported
|
315
|
+
unless tls_enabled?
|
316
|
+
error "534 TLS not enabled"
|
317
|
+
end
|
318
|
+
end
|
319
|
+
|
320
|
+
def tls_enabled?
|
321
|
+
@tls != :off
|
322
|
+
end
|
323
|
+
|
324
|
+
def cmd_cdup(argument)
|
325
|
+
ensure_logged_in
|
326
|
+
cmd_cwd('..')
|
327
|
+
end
|
328
|
+
|
329
|
+
def cmd_pwd(argument)
|
330
|
+
ensure_logged_in
|
331
|
+
pwd
|
332
|
+
end
|
333
|
+
|
334
|
+
def cmd_auth(security_scheme)
|
335
|
+
ensure_tls_supported
|
336
|
+
if @socket.encrypted?
|
337
|
+
error "503 AUTH already done"
|
338
|
+
end
|
339
|
+
unless security_scheme =~ /^TLS(-C)?$/i
|
340
|
+
error "504 Security scheme not implemented: #{security_scheme}"
|
341
|
+
end
|
342
|
+
reply "234 AUTH #{security_scheme} OK."
|
343
|
+
@socket.encrypt
|
344
|
+
end
|
345
|
+
|
346
|
+
def cmd_pbsz(buffer_size)
|
347
|
+
ensure_tls_supported
|
348
|
+
syntax_error unless buffer_size =~ /^\d+$/
|
349
|
+
buffer_size = buffer_size.to_i
|
350
|
+
unless @socket.encrypted?
|
351
|
+
error "503 PBSZ must be preceded by AUTH"
|
352
|
+
end
|
353
|
+
unless buffer_size == 0
|
354
|
+
error "501 PBSZ=0"
|
355
|
+
end
|
356
|
+
reply "200 PBSZ=0"
|
357
|
+
@protection_buffer_size_set = true
|
358
|
+
end
|
359
|
+
|
360
|
+
def cmd_prot(level_arg)
|
361
|
+
level_code = level_arg.upcase
|
362
|
+
unless @protection_buffer_size_set
|
363
|
+
error "503 PROT must be preceded by PBSZ"
|
364
|
+
end
|
365
|
+
level = DATA_CHANNEL_PROTECTION_LEVELS[level_code]
|
366
|
+
unless level
|
367
|
+
error "504 Unknown protection level"
|
368
|
+
end
|
369
|
+
unless level == :private
|
370
|
+
error "536 Unsupported protection level #{level}"
|
371
|
+
end
|
372
|
+
@data_channel_protection_level = level
|
373
|
+
reply "200 Data protection level #{level_code}"
|
374
|
+
end
|
375
|
+
|
376
|
+
def pwd
|
377
|
+
reply %Q(257 "#{@name_prefix}" is current directory)
|
378
|
+
end
|
379
|
+
|
380
|
+
TRANSMISSION_MODES = {
|
381
|
+
'B'=>['Block', false],
|
382
|
+
'C'=>['Compressed', false],
|
383
|
+
'S'=>['Stream', true],
|
384
|
+
}
|
385
|
+
|
386
|
+
FORMAT_TYPES = {
|
387
|
+
'N'=>['Non-print', true],
|
388
|
+
'T'=>['Telnet format effectors', false],
|
389
|
+
'C'=>['Carriage Control (ASA)', false],
|
390
|
+
}
|
391
|
+
|
392
|
+
DATA_TYPES = {
|
393
|
+
'A'=>['ASCII', true],
|
394
|
+
'E'=>['EBCDIC', false],
|
395
|
+
'I'=>['BINARY', true],
|
396
|
+
'L'=>['LOCAL', false],
|
397
|
+
}
|
398
|
+
|
399
|
+
FILE_STRUCTURES = {
|
400
|
+
'R'=>['Record', false],
|
401
|
+
'F'=>['File', true],
|
402
|
+
'P'=>['Page', false],
|
403
|
+
}
|
404
|
+
|
405
|
+
DATA_CHANNEL_PROTECTION_LEVELS = {
|
406
|
+
'C'=>:clear,
|
407
|
+
'S'=>:safe,
|
408
|
+
'E'=>:confidential,
|
409
|
+
'P'=>:private
|
410
|
+
}
|
411
|
+
|
412
|
+
def set_file_system(file_system)
|
413
|
+
@file_system = FileSystemErrorTranslator.new(file_system)
|
414
|
+
end
|
415
|
+
|
416
|
+
def child_path_of?(parent, child)
|
417
|
+
child.cleanpath.to_s.index(parent.cleanpath.to_s) == 0
|
418
|
+
end
|
419
|
+
|
420
|
+
def handle_system_error
|
421
|
+
begin
|
422
|
+
yield
|
423
|
+
rescue SystemCallError => e
|
424
|
+
error "550 #{e}"
|
425
|
+
end
|
426
|
+
end
|
427
|
+
|
428
|
+
def transmit_file(contents, data_type = @data_type)
|
429
|
+
open_data_connection do |data_socket|
|
430
|
+
contents = unix_to_nvt_ascii(contents) if data_type == 'A'
|
431
|
+
data_socket.write(contents)
|
432
|
+
debug("Sent #{contents.size} bytes")
|
433
|
+
reply "226 Transfer complete"
|
434
|
+
end
|
435
|
+
end
|
436
|
+
|
437
|
+
def receive_file(path)
|
438
|
+
open_data_connection do |data_socket|
|
439
|
+
contents = data_socket.read
|
440
|
+
contents = nvt_ascii_to_unix(contents) if @data_type == 'A'
|
441
|
+
debug("Received #{contents.size} bytes")
|
442
|
+
contents
|
443
|
+
end
|
444
|
+
end
|
445
|
+
|
446
|
+
def unix_to_nvt_ascii(s)
|
447
|
+
return s if s =~ /\r\n/
|
448
|
+
s.gsub(/\n/, "\r\n")
|
449
|
+
end
|
450
|
+
|
451
|
+
def nvt_ascii_to_unix(s)
|
452
|
+
s.gsub(/\r\n/, "\n")
|
453
|
+
end
|
454
|
+
|
455
|
+
def open_data_connection(&block)
|
456
|
+
reply "150 Opening #{data_connection_description}"
|
457
|
+
if @data_server
|
458
|
+
if encrypt_data?
|
459
|
+
open_passive_tls_data_connection(&block)
|
460
|
+
else
|
461
|
+
open_passive_data_connection(&block)
|
462
|
+
end
|
463
|
+
else
|
464
|
+
if encrypt_data?
|
465
|
+
open_active_tls_data_connection(&block)
|
466
|
+
else
|
467
|
+
open_active_data_connection(&block)
|
468
|
+
end
|
469
|
+
end
|
470
|
+
end
|
471
|
+
|
472
|
+
def data_connection_description
|
473
|
+
[
|
474
|
+
DATA_TYPES[@data_type][0],
|
475
|
+
"mode data connection",
|
476
|
+
("(TLS)" if encrypt_data?)
|
477
|
+
].compact.join(' ')
|
478
|
+
end
|
479
|
+
|
480
|
+
def encrypt_data?
|
481
|
+
@data_channel_protection_level != :clear
|
482
|
+
end
|
483
|
+
|
484
|
+
def open_active_data_connection
|
485
|
+
data_socket = TCPSocket.new(@data_hostname, @data_port)
|
486
|
+
begin
|
487
|
+
yield(data_socket)
|
488
|
+
ensure
|
489
|
+
data_socket.close
|
490
|
+
end
|
491
|
+
end
|
492
|
+
|
493
|
+
def open_active_tls_data_connection
|
494
|
+
open_active_data_connection do |socket|
|
495
|
+
make_tls_connection(socket) do |ssl_socket|
|
496
|
+
yield(ssl_socket)
|
497
|
+
end
|
498
|
+
end
|
499
|
+
end
|
500
|
+
|
501
|
+
def open_passive_data_connection
|
502
|
+
data_socket = @data_server.accept
|
503
|
+
begin
|
504
|
+
yield(data_socket)
|
505
|
+
ensure
|
506
|
+
data_socket.close
|
507
|
+
end
|
508
|
+
end
|
509
|
+
|
510
|
+
def close_data_server_socket_when_done
|
511
|
+
yield
|
512
|
+
ensure
|
513
|
+
close_data_server_socket
|
514
|
+
end
|
515
|
+
|
516
|
+
def close_data_server_socket
|
517
|
+
return unless @data_server
|
518
|
+
@data_server.close
|
519
|
+
@data_server = nil
|
520
|
+
end
|
521
|
+
|
522
|
+
def open_passive_tls_data_connection
|
523
|
+
open_passive_data_connection do |socket|
|
524
|
+
make_tls_connection(socket) do |ssl_socket|
|
525
|
+
yield(ssl_socket)
|
526
|
+
end
|
527
|
+
end
|
528
|
+
end
|
529
|
+
|
530
|
+
def make_tls_connection(socket)
|
531
|
+
ssl_socket = OpenSSL::SSL::SSLSocket.new(socket, @socket.ssl_context)
|
532
|
+
ssl_socket.accept
|
533
|
+
begin
|
534
|
+
yield(ssl_socket)
|
535
|
+
ensure
|
536
|
+
ssl_socket.close
|
537
|
+
end
|
538
|
+
end
|
539
|
+
|
540
|
+
def get_command
|
541
|
+
s = @socket.gets
|
542
|
+
throw :done if s.nil?
|
543
|
+
s = s.chomp
|
544
|
+
debug(s)
|
545
|
+
s
|
546
|
+
end
|
547
|
+
|
548
|
+
def reply(s)
|
549
|
+
if @response_delay.to_i != 0
|
550
|
+
debug "#{@response_delay} second delay before replying"
|
551
|
+
sleep @response_delay
|
552
|
+
end
|
553
|
+
debug(s)
|
554
|
+
@socket.puts(s)
|
555
|
+
end
|
556
|
+
|
557
|
+
def debug(*s)
|
558
|
+
return unless debug?
|
559
|
+
File.open(@debug_path, 'a') do |file|
|
560
|
+
file.puts(*s)
|
561
|
+
end
|
562
|
+
end
|
563
|
+
|
564
|
+
def debug?
|
565
|
+
@debug || ENV['FTPD_DEBUG'].to_i != 0
|
566
|
+
end
|
567
|
+
|
568
|
+
end
|
569
|
+
end
|