ftpd 0.0.0.pre1 → 0.0.0.pre2
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/README.md +17 -1
- data/VERSION +1 -1
- data/examples/example.rb +77 -0
- data/features/step_definitions/connect.rb +4 -0
- data/features/support/test_client.rb +7 -2
- data/features/support/test_server.rb +4 -4
- data/ftpd.gemspec +7 -8
- data/lib/ftpd.rb +4 -6
- data/lib/ftpd/ftp_server.rb +665 -0
- data/lib/ftpd/server.rb +59 -0
- data/lib/ftpd/temp_dir.rb +56 -0
- data/lib/ftpd/tls_server.rb +57 -0
- metadata +17 -18
- data/lib/ftpd/FakeFtpServer.rb +0 -736
- data/lib/ftpd/FakeServer.rb +0 -57
- data/lib/ftpd/FakeTlsServer.rb +0 -52
- data/lib/ftpd/ObjectUtil.rb +0 -66
- data/lib/ftpd/TempDir.rb +0 -54
- data/lib/ftpd/q.rb +0 -92
data/README.md
CHANGED
@@ -1,7 +1,8 @@
|
|
1
1
|
# FTPD
|
2
2
|
|
3
3
|
ftpd is a pure Ruby FTP server library. It supports implicit and
|
4
|
-
explicit TLS,
|
4
|
+
explicit TLS, suitlble for use by a program such as a test fixture or
|
5
|
+
FTP daemon.
|
5
6
|
|
6
7
|
## UNFINISHED
|
7
8
|
|
@@ -20,6 +21,21 @@ TLS is only supported in passive mode, not active. Either the FTPS
|
|
20
21
|
client used by the test doesn't work in active mode, or this server
|
21
22
|
doesn't work in FTPS active mode (or both).
|
22
23
|
|
24
|
+
## DEVELOPMENT
|
25
|
+
|
26
|
+
### TESTS
|
27
|
+
|
28
|
+
To run the cucumber tests:
|
29
|
+
|
30
|
+
$ rake features
|
31
|
+
|
32
|
+
To run the stand-alone example:
|
33
|
+
|
34
|
+
$ examples/example.rb
|
35
|
+
|
36
|
+
The example prints its port, username and password to the console.
|
37
|
+
You can connect to the stand-alone example with any FTP client.
|
38
|
+
|
23
39
|
## REFERENCES
|
24
40
|
|
25
41
|
(This list of references comes from the README of the em-ftpd gem,
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.0.0.
|
1
|
+
0.0.0.pre2
|
data/examples/example.rb
ADDED
@@ -0,0 +1,77 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
unless $:.include?(File.dirname(__FILE__) + '/../lib')
|
4
|
+
$:.unshift(File.dirname(__FILE__) + '/../lib')
|
5
|
+
end
|
6
|
+
|
7
|
+
require 'ftpd'
|
8
|
+
|
9
|
+
class Example
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
@data_dir = Ftpd::TempDir.new
|
13
|
+
create_files
|
14
|
+
@server = Ftpd::FtpServer.new(@data_dir.path)
|
15
|
+
set_credentials
|
16
|
+
display_connection_info
|
17
|
+
create_connection_script
|
18
|
+
end
|
19
|
+
|
20
|
+
def run
|
21
|
+
wait_until_stopped
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
HOST = 'localhost'
|
27
|
+
|
28
|
+
def create_files
|
29
|
+
create_file 'README',
|
30
|
+
"Temporary directory created by ftpd sample program\n"
|
31
|
+
end
|
32
|
+
|
33
|
+
def create_file(path, contents)
|
34
|
+
full_path = File.expand_path(path, @data_dir.path)
|
35
|
+
FileUtils.mkdir_p File.dirname(full_path)
|
36
|
+
File.open(full_path, 'w') do |file|
|
37
|
+
file.write contents
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def set_credentials
|
42
|
+
@server.user = ENV['LOGNAME']
|
43
|
+
@server.password = ''
|
44
|
+
end
|
45
|
+
|
46
|
+
def display_connection_info
|
47
|
+
puts "Host: #{HOST}"
|
48
|
+
puts "Port: #{@server.port}"
|
49
|
+
puts "User: #{@server.user}"
|
50
|
+
puts "Pass: #{@server.password}"
|
51
|
+
puts "Directory: #{@data_dir.path}"
|
52
|
+
puts "URI: ftp://#{HOST}:#{@server.port}"
|
53
|
+
end
|
54
|
+
|
55
|
+
def create_connection_script
|
56
|
+
command_path = '/tmp/connect-to-example-ftp-server.sh'
|
57
|
+
File.open(command_path, 'w') do |file|
|
58
|
+
file.puts "#!/bin/bash"
|
59
|
+
file.puts "ftp $FTP_ARGS #{HOST} #{@server.port}"
|
60
|
+
end
|
61
|
+
system("chmod +x #{command_path}")
|
62
|
+
puts "Connection script written to #{command_path}"
|
63
|
+
end
|
64
|
+
|
65
|
+
def wait_until_stopped
|
66
|
+
puts "FTP server started. Press ENTER or c-C to stop it"
|
67
|
+
$stdout.flush
|
68
|
+
begin
|
69
|
+
gets
|
70
|
+
rescue Interrupt
|
71
|
+
puts "Interrupt"
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
76
|
+
|
77
|
+
Example.new.run if $0 == __FILE__
|
@@ -7,11 +7,16 @@ class TestClient
|
|
7
7
|
include FileUtils
|
8
8
|
|
9
9
|
def initialize(opts = {})
|
10
|
-
@temp_dir =
|
10
|
+
@temp_dir = Ftpd::TempDir.new
|
11
11
|
@ftp = make_ftp(opts)
|
12
12
|
@templates = TestFileTemplates.new
|
13
13
|
end
|
14
14
|
|
15
|
+
def close
|
16
|
+
@ftp.close
|
17
|
+
@temp_dir.rm
|
18
|
+
end
|
19
|
+
|
15
20
|
def_delegators :@ftp,
|
16
21
|
:chdir,
|
17
22
|
:connect,
|
@@ -61,7 +66,7 @@ class TestClient
|
|
61
66
|
end
|
62
67
|
|
63
68
|
def temp_path(path)
|
64
|
-
File.expand_path(path, @temp_dir)
|
69
|
+
File.expand_path(path, @temp_dir.path)
|
65
70
|
end
|
66
71
|
|
67
72
|
def make_ftp(opts)
|
@@ -7,14 +7,14 @@ class TestServer
|
|
7
7
|
include FileUtils
|
8
8
|
|
9
9
|
def initialize
|
10
|
-
@temp_dir =
|
11
|
-
@server =
|
10
|
+
@temp_dir = Ftpd::TempDir.new
|
11
|
+
@server = Ftpd::FtpServer.new(@temp_dir.path)
|
12
12
|
@templates = TestFileTemplates.new
|
13
13
|
end
|
14
14
|
|
15
15
|
def close
|
16
16
|
@server.close
|
17
|
-
|
17
|
+
@temp_dir.rm
|
18
18
|
end
|
19
19
|
|
20
20
|
def host
|
@@ -46,7 +46,7 @@ class TestServer
|
|
46
46
|
private
|
47
47
|
|
48
48
|
def temp_path(path)
|
49
|
-
File.expand_path(path, @temp_dir)
|
49
|
+
File.expand_path(path, @temp_dir.path)
|
50
50
|
end
|
51
51
|
|
52
52
|
end
|
data/ftpd.gemspec
CHANGED
@@ -5,12 +5,12 @@
|
|
5
5
|
|
6
6
|
Gem::Specification.new do |s|
|
7
7
|
s.name = "ftpd"
|
8
|
-
s.version = "0.0.0.
|
8
|
+
s.version = "0.0.0.pre2"
|
9
9
|
|
10
10
|
s.required_rubygems_version = Gem::Requirement.new("> 1.3.1") if s.respond_to? :required_rubygems_version=
|
11
11
|
s.authors = ["Wayne Conrad"]
|
12
12
|
s.date = "2013-02-10"
|
13
|
-
s.description = "ftpd is a pure Ruby FTP server library. It supports implicit and explicit TLS,
|
13
|
+
s.description = "ftpd is a pure Ruby FTP server library. It supports implicit and explicit TLS, suitlble for use by a program such as a test fixture or FTP daemon."
|
14
14
|
s.email = "wconrad@yagni.com"
|
15
15
|
s.extra_rdoc_files = [
|
16
16
|
"LICENSE.md",
|
@@ -23,6 +23,7 @@ Gem::Specification.new do |s|
|
|
23
23
|
"README.md",
|
24
24
|
"Rakefile",
|
25
25
|
"VERSION",
|
26
|
+
"examples/example.rb",
|
26
27
|
"features/command_errors.feature",
|
27
28
|
"features/delete.feature",
|
28
29
|
"features/directory_navigation.feature",
|
@@ -70,12 +71,10 @@ Gem::Specification.new do |s|
|
|
70
71
|
"ftpd.gemspec",
|
71
72
|
"insecure-test-cert.pem",
|
72
73
|
"lib/ftpd.rb",
|
73
|
-
"lib/ftpd/
|
74
|
-
"lib/ftpd/
|
75
|
-
"lib/ftpd/
|
76
|
-
"lib/ftpd/
|
77
|
-
"lib/ftpd/TempDir.rb",
|
78
|
-
"lib/ftpd/q.rb",
|
74
|
+
"lib/ftpd/ftp_server.rb",
|
75
|
+
"lib/ftpd/server.rb",
|
76
|
+
"lib/ftpd/temp_dir.rb",
|
77
|
+
"lib/ftpd/tls_server.rb",
|
79
78
|
"rake_tasks/cucumber.rake",
|
80
79
|
"rake_tasks/jeweler.rake"
|
81
80
|
]
|
data/lib/ftpd.rb
CHANGED
@@ -1,6 +1,4 @@
|
|
1
|
-
require 'ftpd/
|
2
|
-
require 'ftpd/
|
3
|
-
require 'ftpd/
|
4
|
-
require 'ftpd/
|
5
|
-
require 'ftpd/FakeTlsServer'
|
6
|
-
require 'ftpd/FakeFtpServer'
|
1
|
+
require 'ftpd/ftp_server'
|
2
|
+
require 'ftpd/server'
|
3
|
+
require 'ftpd/temp_dir'
|
4
|
+
require 'ftpd/tls_server'
|
@@ -0,0 +1,665 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'fileutils'
|
4
|
+
require 'openssl'
|
5
|
+
require 'pathname'
|
6
|
+
require File.expand_path('tls_server', File.dirname(__FILE__))
|
7
|
+
require File.expand_path('temp_dir', File.dirname(__FILE__))
|
8
|
+
|
9
|
+
module Ftpd
|
10
|
+
class FtpServer < TlsServer
|
11
|
+
|
12
|
+
attr_accessor :user
|
13
|
+
attr_accessor :password
|
14
|
+
attr_accessor :debug_path
|
15
|
+
attr_accessor :response_delay
|
16
|
+
attr_accessor :implicit_tls
|
17
|
+
|
18
|
+
def initialize(data_path)
|
19
|
+
super()
|
20
|
+
self.user = 'user'
|
21
|
+
self.password = 'password'
|
22
|
+
self.debug_path = '/dev/stdout'
|
23
|
+
@data_path = Pathname.new(data_path)
|
24
|
+
@response_delay = 0
|
25
|
+
@implicit_tls = false
|
26
|
+
end
|
27
|
+
|
28
|
+
def session(socket)
|
29
|
+
Session.new(:socket => socket,
|
30
|
+
:user => user,
|
31
|
+
:password => password,
|
32
|
+
:data_path => @data_path,
|
33
|
+
:debug_path => debug_path,
|
34
|
+
:response_delay => response_delay,
|
35
|
+
:implicit_tls => @implicit_tls).run
|
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
|
+
|
662
|
+
end
|
663
|
+
|
664
|
+
end
|
665
|
+
end
|