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/error.rb
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
module Ftpd
|
2
|
+
|
3
|
+
# Translate specific exceptions to FileSystemError.
|
4
|
+
|
5
|
+
class ExceptionTranslator
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@exceptions = []
|
9
|
+
end
|
10
|
+
|
11
|
+
# Register an exception class.
|
12
|
+
|
13
|
+
def register_exception(e)
|
14
|
+
@exceptions << e
|
15
|
+
end
|
16
|
+
|
17
|
+
# Run a block, translating specific exceptions to FileSystemError.
|
18
|
+
|
19
|
+
def translate_exceptions
|
20
|
+
begin
|
21
|
+
return yield
|
22
|
+
rescue *@exceptions => e
|
23
|
+
raise FileSystemError, e.message
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module Ftpd
|
2
|
+
|
3
|
+
class FtpServerError < StandardError ; end
|
4
|
+
|
5
|
+
def self.ftp_server_error(class_name)
|
6
|
+
const_set class_name, Class.new(FtpServerError)
|
7
|
+
end
|
8
|
+
|
9
|
+
ftp_server_error :CommandError
|
10
|
+
ftp_server_error :DriverError
|
11
|
+
ftp_server_error :FileSystemError
|
12
|
+
|
13
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Ftpd
|
2
|
+
class FileSystemErrorTranslator
|
3
|
+
|
4
|
+
include Error
|
5
|
+
|
6
|
+
def initialize(file_system)
|
7
|
+
@file_system = file_system
|
8
|
+
end
|
9
|
+
|
10
|
+
def respond_to?(method)
|
11
|
+
@file_system.respond_to?(method)
|
12
|
+
end
|
13
|
+
|
14
|
+
def method_missing(method, *args)
|
15
|
+
@file_system.send(method, *args)
|
16
|
+
rescue FileSystemError => e
|
17
|
+
error "450 #{e}"
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
end
|
data/lib/ftpd/ftp_server.rb
CHANGED
@@ -1,664 +1,27 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
|
3
|
-
require 'fileutils'
|
4
|
-
require 'openssl'
|
5
|
-
require 'pathname'
|
6
|
-
require 'ftpd/tls_server'
|
7
|
-
require 'ftpd/temp_dir'
|
8
|
-
|
9
3
|
module Ftpd
|
10
4
|
class FtpServer < TlsServer
|
11
5
|
|
12
|
-
attr_accessor :user
|
13
|
-
attr_accessor :password
|
14
6
|
attr_accessor :debug_path
|
7
|
+
attr_accessor :debug
|
15
8
|
attr_accessor :response_delay
|
16
|
-
attr_accessor :implicit_tls
|
17
9
|
|
18
|
-
def initialize(
|
10
|
+
def initialize(driver)
|
19
11
|
super()
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
@data_path = Pathname.new(data_path)
|
12
|
+
@driver = driver
|
13
|
+
@debug_path = '/dev/stdout'
|
14
|
+
@debug = false
|
24
15
|
@response_delay = 0
|
25
|
-
@implicit_tls = false
|
26
16
|
end
|
27
17
|
|
28
18
|
def session(socket)
|
29
19
|
Session.new(:socket => socket,
|
30
|
-
:
|
31
|
-
:
|
32
|
-
:data_path => @data_path,
|
20
|
+
:driver => @driver,
|
21
|
+
:debug => @debug,
|
33
22
|
:debug_path => debug_path,
|
34
23
|
:response_delay => response_delay,
|
35
|
-
:
|
36
|
-
end
|
37
|
-
|
38
|
-
private
|
39
|
-
|
40
|
-
class Session
|
41
|
-
|
42
|
-
def initialize(args)
|
43
|
-
@socket = args[:socket]
|
44
|
-
@socket.encrypt if args[:implicit_tls]
|
45
|
-
@expected_user = args[:user]
|
46
|
-
@expected_password = args[:password]
|
47
|
-
@data_path = @cwd = args[:data_path].realpath
|
48
|
-
@debug_path = args[:debug_path]
|
49
|
-
@data_type = 'A'
|
50
|
-
@mode = 'S'
|
51
|
-
@format = 'N'
|
52
|
-
@structure = 'F'
|
53
|
-
@response_delay = args[:response_delay]
|
54
|
-
@data_channel_protection_level = :clear
|
55
|
-
end
|
56
|
-
|
57
|
-
def run
|
58
|
-
reply "220 FakeFtpServer"
|
59
|
-
@state = :user
|
60
|
-
catch :done do
|
61
|
-
loop do
|
62
|
-
begin
|
63
|
-
s = get_command
|
64
|
-
syntax_error unless s =~ /^(\w+)(?: (.*))?$/
|
65
|
-
command, argument = $1.downcase, $2
|
66
|
-
unless VALID_COMMANDS.include?(command)
|
67
|
-
error "500 Syntax error, command unrecognized: #{s}"
|
68
|
-
end
|
69
|
-
method = 'cmd_' + command
|
70
|
-
unless self.class.private_method_defined?(method)
|
71
|
-
error "502 Command not implemented: #{command}"
|
72
|
-
end
|
73
|
-
send(method, argument)
|
74
|
-
rescue Error => e
|
75
|
-
reply e.message
|
76
|
-
rescue Errno::ECONNRESET, Errno::EPIPE
|
77
|
-
end
|
78
|
-
end
|
79
|
-
end
|
80
|
-
end
|
81
|
-
|
82
|
-
private
|
83
|
-
|
84
|
-
class Error < StandardError
|
85
|
-
end
|
86
|
-
|
87
|
-
VALID_COMMANDS = [
|
88
|
-
"abor",
|
89
|
-
"acct",
|
90
|
-
"allo",
|
91
|
-
"appe",
|
92
|
-
"auth",
|
93
|
-
"pbsz",
|
94
|
-
"cdup",
|
95
|
-
"cwd",
|
96
|
-
"dele",
|
97
|
-
"help",
|
98
|
-
"list",
|
99
|
-
"mkd",
|
100
|
-
"mode",
|
101
|
-
"nlst",
|
102
|
-
"noop",
|
103
|
-
"pass",
|
104
|
-
"pasv",
|
105
|
-
"port",
|
106
|
-
"prot",
|
107
|
-
"pwd",
|
108
|
-
"quit",
|
109
|
-
"rein",
|
110
|
-
"rest",
|
111
|
-
"retr",
|
112
|
-
"rmd",
|
113
|
-
"rnfr",
|
114
|
-
"rnto",
|
115
|
-
"site",
|
116
|
-
"smnt",
|
117
|
-
"stat",
|
118
|
-
"stor",
|
119
|
-
"stou",
|
120
|
-
"stru",
|
121
|
-
"syst",
|
122
|
-
"type",
|
123
|
-
"user",
|
124
|
-
]
|
125
|
-
|
126
|
-
def cmd_user(argument)
|
127
|
-
syntax_error unless argument
|
128
|
-
bad_sequence unless @state == :user
|
129
|
-
@user = argument
|
130
|
-
@state = :password
|
131
|
-
reply "331 Password required"
|
132
|
-
end
|
133
|
-
|
134
|
-
def bad_sequence
|
135
|
-
error "503 Bad sequence of commands"
|
136
|
-
end
|
137
|
-
|
138
|
-
def cmd_pass(argument)
|
139
|
-
syntax_error unless argument
|
140
|
-
bad_sequence unless @state == :password
|
141
|
-
password = argument
|
142
|
-
if @user != @expected_user || password != @expected_password
|
143
|
-
@state = :user
|
144
|
-
error "530 Login incorrect"
|
145
|
-
end
|
146
|
-
reply "230 Logged in"
|
147
|
-
@state = :logged_in
|
148
|
-
end
|
149
|
-
|
150
|
-
def cmd_quit(argument)
|
151
|
-
syntax_error if argument
|
152
|
-
check_logged_in
|
153
|
-
reply "221 Byebye"
|
154
|
-
@state = :user
|
155
|
-
end
|
156
|
-
|
157
|
-
def syntax_error
|
158
|
-
error "501 Syntax error"
|
159
|
-
end
|
160
|
-
|
161
|
-
def cmd_port(argument)
|
162
|
-
check_logged_in
|
163
|
-
pieces = argument.split(/,/)
|
164
|
-
syntax_error unless pieces.size == 6
|
165
|
-
pieces.collect! do |s|
|
166
|
-
syntax_error unless s =~ /^\d{1,3}$/
|
167
|
-
i = s.to_i
|
168
|
-
syntax_error unless (0..255) === i
|
169
|
-
i
|
170
|
-
end
|
171
|
-
@data_hostname = pieces[0..3].join('.')
|
172
|
-
@data_port = pieces[4] << 8 | pieces[5]
|
173
|
-
reply "200 PORT command successful"
|
174
|
-
end
|
175
|
-
|
176
|
-
def cmd_stor(argument)
|
177
|
-
close_data_server_socket_when_done do
|
178
|
-
check_logged_in
|
179
|
-
path = argument
|
180
|
-
syntax_error unless path
|
181
|
-
target = target_path(path)
|
182
|
-
ensure_path_is_in_data_dir(target)
|
183
|
-
contents = receive_file(path)
|
184
|
-
write_file(target, contents)
|
185
|
-
reply "226 Transfer complete"
|
186
|
-
end
|
187
|
-
end
|
188
|
-
|
189
|
-
def cmd_retr(argument)
|
190
|
-
close_data_server_socket_when_done do
|
191
|
-
check_logged_in
|
192
|
-
path = argument
|
193
|
-
syntax_error unless path
|
194
|
-
target = target_path(path)
|
195
|
-
ensure_path_is_in_data_dir(target)
|
196
|
-
contents = read_file(target)
|
197
|
-
transmit_file(contents)
|
198
|
-
end
|
199
|
-
end
|
200
|
-
|
201
|
-
def cmd_dele(argument)
|
202
|
-
check_logged_in
|
203
|
-
path = argument
|
204
|
-
error "501 Path required" unless path
|
205
|
-
target = target_path(path)
|
206
|
-
ensure_path_is_in_data_dir(target)
|
207
|
-
ensure_path_exists target
|
208
|
-
File.unlink(target)
|
209
|
-
reply "250 DELE command successful"
|
210
|
-
end
|
211
|
-
|
212
|
-
def cmd_list(argument)
|
213
|
-
ls(argument, '-l')
|
214
|
-
end
|
215
|
-
|
216
|
-
def cmd_nlst(argument)
|
217
|
-
ls(argument, '-1')
|
218
|
-
end
|
219
|
-
|
220
|
-
def ls(path, option)
|
221
|
-
close_data_server_socket_when_done do
|
222
|
-
check_logged_in
|
223
|
-
ls_dir, ls_path = get_ls_dir_and_path(path)
|
224
|
-
list = get_file_list(ls_dir, ls_path, option)
|
225
|
-
transmit_file(list, 'A')
|
226
|
-
end
|
227
|
-
end
|
228
|
-
|
229
|
-
def get_ls_dir_and_path(path)
|
230
|
-
path = path || '.'
|
231
|
-
target = target_path(path)
|
232
|
-
target = realpath(target)
|
233
|
-
ensure_path_is_in_data_dir(target)
|
234
|
-
if target.to_s.index(@cwd.to_s) == 0
|
235
|
-
ls_dir = @cwd
|
236
|
-
ls_path = target.to_s[@cwd.to_s.length..-1]
|
237
|
-
else
|
238
|
-
raise
|
239
|
-
end
|
240
|
-
if ls_path =~ /^\//
|
241
|
-
ls_path = $'
|
242
|
-
end
|
243
|
-
[ls_dir, ls_path]
|
244
|
-
end
|
245
|
-
|
246
|
-
def get_file_list(ls_dir, ls_path, option)
|
247
|
-
command = [
|
248
|
-
'ls',
|
249
|
-
option,
|
250
|
-
ls_path,
|
251
|
-
'2>&1',
|
252
|
-
].compact.join(' ')
|
253
|
-
list = Dir.chdir(ls_dir) do
|
254
|
-
`#{command}`
|
255
|
-
end
|
256
|
-
list = "" if $? != 0
|
257
|
-
list = list.gsub(/^total \d+\n/, '')
|
258
|
-
list
|
259
|
-
end
|
260
|
-
|
261
|
-
def realpath(pathname)
|
262
|
-
handle_system_error do
|
263
|
-
basename = File.basename(pathname.to_s)
|
264
|
-
if is_glob?(basename)
|
265
|
-
pathname.dirname.realpath + basename
|
266
|
-
else
|
267
|
-
pathname.realpath
|
268
|
-
end
|
269
|
-
end
|
270
|
-
end
|
271
|
-
|
272
|
-
def is_glob?(filename)
|
273
|
-
filename =~ /[.*]/
|
274
|
-
end
|
275
|
-
|
276
|
-
def cmd_type(argument)
|
277
|
-
check_logged_in
|
278
|
-
syntax_error unless argument =~ /^(\S)(?: (\S+))?$/
|
279
|
-
type_code = $1
|
280
|
-
format_code = $2
|
281
|
-
set_type(type_code)
|
282
|
-
set_format(format_code)
|
283
|
-
reply "200 Type set to #{@data_type}"
|
284
|
-
end
|
285
|
-
|
286
|
-
def set_type(type_code)
|
287
|
-
name, implemented = DATA_TYPES[type_code]
|
288
|
-
error "504 Invalid type code" unless name
|
289
|
-
error "504 Type not implemented" unless implemented
|
290
|
-
@data_type = type_code
|
291
|
-
end
|
292
|
-
|
293
|
-
def set_format(format_code)
|
294
|
-
format_code ||= 'N'
|
295
|
-
name, implemented = FORMAT_TYPES[format_code]
|
296
|
-
error "504 Invalid format code" unless name
|
297
|
-
error "504 Format not implemented" unless implemented
|
298
|
-
@data_format = format_code
|
299
|
-
end
|
300
|
-
|
301
|
-
def cmd_mode(argument)
|
302
|
-
syntax_error unless argument
|
303
|
-
check_logged_in
|
304
|
-
name, implemented = TRANSMISSION_MODES[argument]
|
305
|
-
error "504 Invalid mode code" unless name
|
306
|
-
error "504 Mode not implemented" unless implemented
|
307
|
-
@mode = argument
|
308
|
-
reply "200 Mode set to #{name}"
|
309
|
-
end
|
310
|
-
|
311
|
-
def cmd_stru(argument)
|
312
|
-
syntax_error unless argument
|
313
|
-
check_logged_in
|
314
|
-
name, implemented = FILE_STRUCTURES[argument]
|
315
|
-
error "504 Invalid structure code" unless name
|
316
|
-
error "504 Structure not implemented" unless implemented
|
317
|
-
@structure = argument
|
318
|
-
reply "200 File structure set to #{name}"
|
319
|
-
end
|
320
|
-
|
321
|
-
def cmd_noop(argument)
|
322
|
-
syntax_error if argument
|
323
|
-
reply "200 Nothing done"
|
324
|
-
end
|
325
|
-
|
326
|
-
def cmd_pasv(argument)
|
327
|
-
check_logged_in
|
328
|
-
if @data_server
|
329
|
-
reply "200 Already in passive mode"
|
330
|
-
else
|
331
|
-
@data_server = TCPServer.new('localhost', 0)
|
332
|
-
ip = @data_server.addr[3]
|
333
|
-
port = @data_server.addr[1]
|
334
|
-
quads = [
|
335
|
-
ip.scan(/\d+/),
|
336
|
-
port >> 8,
|
337
|
-
port & 0xff,
|
338
|
-
].flatten.join(',')
|
339
|
-
reply "227 Entering passive mode (#{quads})"
|
340
|
-
end
|
341
|
-
end
|
342
|
-
|
343
|
-
def cmd_cwd(argument)
|
344
|
-
check_logged_in
|
345
|
-
target = if argument =~ %r"^/(.*)$"
|
346
|
-
@data_path + $1
|
347
|
-
else
|
348
|
-
@cwd + argument
|
349
|
-
end
|
350
|
-
ensure_path_is_in_data_dir(target)
|
351
|
-
restore_cwd_on_error do
|
352
|
-
@cwd = target
|
353
|
-
pwd
|
354
|
-
end
|
355
|
-
end
|
356
|
-
|
357
|
-
def cmd_cdup(argument)
|
358
|
-
check_logged_in
|
359
|
-
cmd_cwd('..')
|
360
|
-
end
|
361
|
-
|
362
|
-
def cmd_pwd(argument)
|
363
|
-
check_logged_in
|
364
|
-
pwd
|
365
|
-
end
|
366
|
-
|
367
|
-
def cmd_auth(security_scheme)
|
368
|
-
if @socket.encrypted?
|
369
|
-
raise Error, "503 AUTH already done"
|
370
|
-
end
|
371
|
-
unless security_scheme =~ /^TLS(-C)?$/i
|
372
|
-
raise Error, "500 Security scheme not implemented: #{security_scheme}"
|
373
|
-
end
|
374
|
-
reply "234 AUTH #{security_scheme} OK."
|
375
|
-
@socket.encrypt
|
376
|
-
end
|
377
|
-
|
378
|
-
def cmd_pbsz(buffer_size)
|
379
|
-
syntax_error unless buffer_size =~ /^\d+$/
|
380
|
-
buffer_size = buffer_size.to_i
|
381
|
-
unless @socket.encrypted?
|
382
|
-
raise Error, "503 PBSZ must be preceded by AUTH"
|
383
|
-
end
|
384
|
-
unless buffer_size == 0
|
385
|
-
raise Error, "501 PBSZ=0"
|
386
|
-
end
|
387
|
-
reply "200 PBSZ=0"
|
388
|
-
@protection_buffer_size_set = true
|
389
|
-
end
|
390
|
-
|
391
|
-
def cmd_prot(level_arg)
|
392
|
-
level_code = level_arg.upcase
|
393
|
-
unless @protection_buffer_size_set
|
394
|
-
raise Error, "503 PROT must be preceded by PBSZ"
|
395
|
-
end
|
396
|
-
level = DATA_CHANNEL_PROTECTION_LEVELS[level_code]
|
397
|
-
unless level
|
398
|
-
raise Error, "504 Unknown protection level"
|
399
|
-
end
|
400
|
-
unless level == :private
|
401
|
-
raise Error, "536 Unsupported protection level #{level}"
|
402
|
-
end
|
403
|
-
@data_channel_protection_level = level
|
404
|
-
reply "200 Data protection level #{level_code}"
|
405
|
-
end
|
406
|
-
|
407
|
-
def pwd
|
408
|
-
reply %Q(257 "#{sanitized_cwd}" is current directory)
|
409
|
-
end
|
410
|
-
|
411
|
-
def relative_to_data_path(path)
|
412
|
-
data_path = realpath(@data_path).to_s
|
413
|
-
path = realpath(path).to_s
|
414
|
-
path = path.gsub(data_path, '')
|
415
|
-
path = '/' if path.empty?
|
416
|
-
path
|
417
|
-
end
|
418
|
-
|
419
|
-
def sanitized_cwd
|
420
|
-
relative_to_data_path(@cwd)
|
421
|
-
end
|
422
|
-
|
423
|
-
def error(message)
|
424
|
-
raise Error, message
|
425
|
-
end
|
426
|
-
|
427
|
-
TRANSMISSION_MODES = {
|
428
|
-
'B'=>['Block', false],
|
429
|
-
'C'=>['Compressed', false],
|
430
|
-
'S'=>['Stream', true],
|
431
|
-
}
|
432
|
-
|
433
|
-
FORMAT_TYPES = {
|
434
|
-
'N'=>['Non-print', true],
|
435
|
-
'T'=>['Telnet format effectors', false],
|
436
|
-
'C'=>['Carriage Control (ASA)', false],
|
437
|
-
}
|
438
|
-
|
439
|
-
DATA_TYPES = {
|
440
|
-
'A'=>['ASCII', true],
|
441
|
-
'E'=>['EBCDIC', false],
|
442
|
-
'I'=>['BINARY', true],
|
443
|
-
'L'=>['LOCAL', false],
|
444
|
-
}
|
445
|
-
|
446
|
-
FILE_STRUCTURES = {
|
447
|
-
'R'=>['Record', false],
|
448
|
-
'F'=>['File', true],
|
449
|
-
'P'=>['Page', false],
|
450
|
-
}
|
451
|
-
|
452
|
-
DATA_CHANNEL_PROTECTION_LEVELS = {
|
453
|
-
'C'=>:clear,
|
454
|
-
'S'=>:safe,
|
455
|
-
'E'=>:confidential,
|
456
|
-
'P'=>:private
|
457
|
-
}
|
458
|
-
|
459
|
-
def check_logged_in
|
460
|
-
return if @state == :logged_in
|
461
|
-
error "530 Not logged in"
|
462
|
-
end
|
463
|
-
|
464
|
-
def ensure_path_is_in_data_dir(path)
|
465
|
-
unless child_path_of?(@data_path, path)
|
466
|
-
error "550 Access denied"
|
467
|
-
end
|
468
|
-
end
|
469
|
-
|
470
|
-
def ensure_path_exists(path)
|
471
|
-
unless File.exists?(path)
|
472
|
-
error '450 No such file or directory'
|
473
|
-
end
|
474
|
-
end
|
475
|
-
|
476
|
-
def child_path_of?(parent, child)
|
477
|
-
child.cleanpath.to_s.index(parent.cleanpath.to_s) == 0
|
478
|
-
end
|
479
|
-
|
480
|
-
def target_path(path)
|
481
|
-
path = Pathname.new(path)
|
482
|
-
base, path = if path.to_s =~ /^\/(.*)/
|
483
|
-
[@data_path, $1]
|
484
|
-
else
|
485
|
-
[@cwd, path]
|
486
|
-
end
|
487
|
-
base + path
|
488
|
-
end
|
489
|
-
|
490
|
-
def read_file(path)
|
491
|
-
handle_system_error do
|
492
|
-
File.open(path, 'rb') do |file|
|
493
|
-
file.read
|
494
|
-
end
|
495
|
-
end
|
496
|
-
end
|
497
|
-
|
498
|
-
def write_file(dest, contents)
|
499
|
-
handle_system_error do
|
500
|
-
File.open(dest, 'w') do |file|
|
501
|
-
file.write(contents)
|
502
|
-
end
|
503
|
-
end
|
504
|
-
end
|
505
|
-
|
506
|
-
def handle_system_error
|
507
|
-
begin
|
508
|
-
yield
|
509
|
-
rescue SystemCallError => e
|
510
|
-
error "550 #{e}"
|
511
|
-
end
|
512
|
-
end
|
513
|
-
|
514
|
-
def transmit_file(contents, data_type = @data_type)
|
515
|
-
open_data_connection do |data_socket|
|
516
|
-
contents = unix_to_nvt_ascii(contents) if data_type == 'A'
|
517
|
-
data_socket.write(contents)
|
518
|
-
debug("Sent #{contents.size} bytes")
|
519
|
-
reply "226 Transfer complete"
|
520
|
-
end
|
521
|
-
end
|
522
|
-
|
523
|
-
def receive_file(path)
|
524
|
-
open_data_connection do |data_socket|
|
525
|
-
contents = data_socket.read
|
526
|
-
contents = nvt_ascii_to_unix(contents) if @data_type == 'A'
|
527
|
-
debug("Received #{contents.size} bytes")
|
528
|
-
contents
|
529
|
-
end
|
530
|
-
end
|
531
|
-
|
532
|
-
def unix_to_nvt_ascii(s)
|
533
|
-
return s if s =~ /\r\n/
|
534
|
-
s.gsub(/\n/, "\r\n")
|
535
|
-
end
|
536
|
-
|
537
|
-
def nvt_ascii_to_unix(s)
|
538
|
-
s.gsub(/\r\n/, "\n")
|
539
|
-
end
|
540
|
-
|
541
|
-
def open_data_connection(&block)
|
542
|
-
reply "150 Opening #{data_connection_description}"
|
543
|
-
if @data_server
|
544
|
-
if encrypt_data?
|
545
|
-
open_passive_tls_data_connection(&block)
|
546
|
-
else
|
547
|
-
open_passive_data_connection(&block)
|
548
|
-
end
|
549
|
-
else
|
550
|
-
if encrypt_data?
|
551
|
-
open_active_tls_data_connection(&block)
|
552
|
-
else
|
553
|
-
open_active_data_connection(&block)
|
554
|
-
end
|
555
|
-
end
|
556
|
-
end
|
557
|
-
|
558
|
-
def data_connection_description
|
559
|
-
[
|
560
|
-
DATA_TYPES[@data_type][0],
|
561
|
-
"mode data connection",
|
562
|
-
("(TLS)" if encrypt_data?)
|
563
|
-
].compact.join(' ')
|
564
|
-
end
|
565
|
-
|
566
|
-
def encrypt_data?
|
567
|
-
@data_channel_protection_level != :clear
|
568
|
-
end
|
569
|
-
|
570
|
-
def open_active_data_connection
|
571
|
-
data_socket = TCPSocket.new(@data_hostname, @data_port)
|
572
|
-
begin
|
573
|
-
yield(data_socket)
|
574
|
-
ensure
|
575
|
-
data_socket.close
|
576
|
-
end
|
577
|
-
end
|
578
|
-
|
579
|
-
def open_active_tls_data_connection
|
580
|
-
open_active_data_connection do |socket|
|
581
|
-
make_tls_connection(socket) do |ssl_socket|
|
582
|
-
yield(ssl_socket)
|
583
|
-
end
|
584
|
-
end
|
585
|
-
end
|
586
|
-
|
587
|
-
def open_passive_data_connection
|
588
|
-
data_socket = @data_server.accept
|
589
|
-
begin
|
590
|
-
yield(data_socket)
|
591
|
-
ensure
|
592
|
-
data_socket.close
|
593
|
-
end
|
594
|
-
end
|
595
|
-
|
596
|
-
def close_data_server_socket_when_done
|
597
|
-
yield
|
598
|
-
ensure
|
599
|
-
close_data_server_socket
|
600
|
-
end
|
601
|
-
|
602
|
-
def close_data_server_socket
|
603
|
-
return unless @data_server
|
604
|
-
@data_server.close
|
605
|
-
@data_server = nil
|
606
|
-
end
|
607
|
-
|
608
|
-
def open_passive_tls_data_connection
|
609
|
-
open_passive_data_connection do |socket|
|
610
|
-
make_tls_connection(socket) do |ssl_socket|
|
611
|
-
yield(ssl_socket)
|
612
|
-
end
|
613
|
-
end
|
614
|
-
end
|
615
|
-
|
616
|
-
def make_tls_connection(socket)
|
617
|
-
ssl_socket = OpenSSL::SSL::SSLSocket.new(socket, @socket.ssl_context)
|
618
|
-
ssl_socket.accept
|
619
|
-
begin
|
620
|
-
yield(ssl_socket)
|
621
|
-
ensure
|
622
|
-
ssl_socket.close
|
623
|
-
end
|
624
|
-
end
|
625
|
-
|
626
|
-
def get_command
|
627
|
-
s = @socket.gets
|
628
|
-
throw :done if s.nil?
|
629
|
-
s = s.chomp
|
630
|
-
debug(s)
|
631
|
-
s
|
632
|
-
end
|
633
|
-
|
634
|
-
def reply(s)
|
635
|
-
if @response_delay.to_i != 0
|
636
|
-
debug "#{@response_delay} second delay before replying"
|
637
|
-
sleep @response_delay
|
638
|
-
end
|
639
|
-
debug(s)
|
640
|
-
@socket.puts(s)
|
641
|
-
end
|
642
|
-
|
643
|
-
def debug(*s)
|
644
|
-
return unless debug?
|
645
|
-
File.open(@debug_path, 'a') do |file|
|
646
|
-
file.puts(*s)
|
647
|
-
end
|
648
|
-
end
|
649
|
-
|
650
|
-
def debug?
|
651
|
-
ENV['DEBUG'].to_i != 0
|
652
|
-
end
|
653
|
-
|
654
|
-
def restore_cwd_on_error
|
655
|
-
orig_cwd = @cwd
|
656
|
-
yield
|
657
|
-
rescue
|
658
|
-
@cwd = orig_cwd
|
659
|
-
raise
|
660
|
-
end
|
661
|
-
|
24
|
+
:tls => @tls).run
|
662
25
|
end
|
663
26
|
|
664
27
|
end
|