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/lib/ftpd/server.rb
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
require 'socket'
|
2
|
+
|
3
|
+
module Ftpd
|
4
|
+
class Server
|
5
|
+
|
6
|
+
def initialize
|
7
|
+
@server_socket = make_server_socket
|
8
|
+
@server_thread = make_server_thread
|
9
|
+
end
|
10
|
+
|
11
|
+
def port
|
12
|
+
@server_socket.addr[1]
|
13
|
+
end
|
14
|
+
|
15
|
+
def close
|
16
|
+
# An apparent race condition causes this to sometimes not stop the
|
17
|
+
# thread. When this happens, the thread remains blocked in the
|
18
|
+
# accept method; I hypothesize that this happens whenever the
|
19
|
+
# close happens first. Once this bug is fixed, join on the
|
20
|
+
# thread.
|
21
|
+
@server_socket.close
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def make_server_socket
|
27
|
+
return TCPServer.new('localhost', 0)
|
28
|
+
end
|
29
|
+
|
30
|
+
def make_server_thread
|
31
|
+
Thread.new do
|
32
|
+
Thread.abort_on_exception = true
|
33
|
+
loop do
|
34
|
+
begin
|
35
|
+
begin
|
36
|
+
socket = accept
|
37
|
+
rescue Errno::EAGAIN, Errno::ECONNABORTED, Errno::EPROTO, Errno::EINVAL
|
38
|
+
IO.select([@server_socket])
|
39
|
+
sleep(0.2)
|
40
|
+
retry
|
41
|
+
end
|
42
|
+
begin
|
43
|
+
session(socket)
|
44
|
+
ensure
|
45
|
+
socket.close
|
46
|
+
end
|
47
|
+
rescue IOError
|
48
|
+
break
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def accept
|
55
|
+
@server_socket.accept
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'tempfile'
|
2
|
+
|
3
|
+
module Ftpd
|
4
|
+
class TempDir
|
5
|
+
|
6
|
+
attr_reader :path
|
7
|
+
|
8
|
+
class << self
|
9
|
+
|
10
|
+
def make(basename = nil)
|
11
|
+
temp_dir = TempDir.new(basename)
|
12
|
+
begin
|
13
|
+
yield(temp_dir)
|
14
|
+
ensure
|
15
|
+
temp_dir.rm unless temp_dir.kept
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
|
21
|
+
attr_reader :kept
|
22
|
+
|
23
|
+
def initialize(basename = nil)
|
24
|
+
@path = unique_path(basename)
|
25
|
+
@kept = false
|
26
|
+
ObjectSpace.define_finalizer(self, TempDir.cleanup(path))
|
27
|
+
Dir.mkdir(@path)
|
28
|
+
end
|
29
|
+
|
30
|
+
def keep
|
31
|
+
@kept = true
|
32
|
+
ObjectSpace.undefine_finalizer(self)
|
33
|
+
end
|
34
|
+
|
35
|
+
def rm
|
36
|
+
keep
|
37
|
+
system("rm -rf #{path.inspect}")
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def unique_path(basename)
|
43
|
+
tempfile = Tempfile.new(File.basename(basename || $0 || ''))
|
44
|
+
path = tempfile.path
|
45
|
+
tempfile.close!
|
46
|
+
path
|
47
|
+
end
|
48
|
+
|
49
|
+
def TempDir.cleanup(path)
|
50
|
+
proc { |id|
|
51
|
+
system("/bin/rm -rf #{path.inspect}")
|
52
|
+
}
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
require 'openssl'
|
2
|
+
require File.expand_path('server', File.dirname(__FILE__))
|
3
|
+
|
4
|
+
module Ftpd
|
5
|
+
class TlsServer < Server
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@ssl_context = make_ssl_context
|
9
|
+
super
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
def make_server_socket
|
15
|
+
ssl_server_socket = OpenSSL::SSL::SSLServer.new(super, @ssl_context);
|
16
|
+
ssl_server_socket.start_immediately = false
|
17
|
+
ssl_server_socket
|
18
|
+
end
|
19
|
+
|
20
|
+
def accept
|
21
|
+
socket = @server_socket.accept
|
22
|
+
add_tls_methods_to_socket(socket)
|
23
|
+
socket
|
24
|
+
end
|
25
|
+
|
26
|
+
def make_ssl_context
|
27
|
+
context = OpenSSL::SSL::SSLContext.new
|
28
|
+
File.open(certfile_path) do |certfile|
|
29
|
+
context.cert = OpenSSL::X509::Certificate.new(certfile)
|
30
|
+
certfile.rewind
|
31
|
+
context.key = OpenSSL::PKey::RSA.new(certfile)
|
32
|
+
end
|
33
|
+
context
|
34
|
+
end
|
35
|
+
|
36
|
+
def certfile_path
|
37
|
+
File.expand_path('../../insecure-test-cert.pem',
|
38
|
+
File.dirname(__FILE__))
|
39
|
+
end
|
40
|
+
|
41
|
+
def add_tls_methods_to_socket(socket)
|
42
|
+
context = @ssl_context
|
43
|
+
class << socket
|
44
|
+
def ssl_context
|
45
|
+
context
|
46
|
+
end
|
47
|
+
def encrypted?
|
48
|
+
!!cipher
|
49
|
+
end
|
50
|
+
def encrypt
|
51
|
+
accept
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ftpd
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.0.
|
4
|
+
version: 0.0.0.pre2
|
5
5
|
prerelease: 6
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -13,7 +13,7 @@ date: 2013-02-10 00:00:00.000000000 Z
|
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: cucumber
|
16
|
-
requirement: &
|
16
|
+
requirement: &70369190 !ruby/object:Gem::Requirement
|
17
17
|
none: false
|
18
18
|
requirements:
|
19
19
|
- - ~>
|
@@ -21,10 +21,10 @@ dependencies:
|
|
21
21
|
version: 1.2.1
|
22
22
|
type: :development
|
23
23
|
prerelease: false
|
24
|
-
version_requirements: *
|
24
|
+
version_requirements: *70369190
|
25
25
|
- !ruby/object:Gem::Dependency
|
26
26
|
name: double-bag-ftps
|
27
|
-
requirement: &
|
27
|
+
requirement: &70366630 !ruby/object:Gem::Requirement
|
28
28
|
none: false
|
29
29
|
requirements:
|
30
30
|
- - ~>
|
@@ -32,10 +32,10 @@ dependencies:
|
|
32
32
|
version: 0.1.0
|
33
33
|
type: :development
|
34
34
|
prerelease: false
|
35
|
-
version_requirements: *
|
35
|
+
version_requirements: *70366630
|
36
36
|
- !ruby/object:Gem::Dependency
|
37
37
|
name: jeweler
|
38
|
-
requirement: &
|
38
|
+
requirement: &70397920 !ruby/object:Gem::Requirement
|
39
39
|
none: false
|
40
40
|
requirements:
|
41
41
|
- - ~>
|
@@ -43,10 +43,10 @@ dependencies:
|
|
43
43
|
version: 1.8.4
|
44
44
|
type: :development
|
45
45
|
prerelease: false
|
46
|
-
version_requirements: *
|
46
|
+
version_requirements: *70397920
|
47
47
|
- !ruby/object:Gem::Dependency
|
48
48
|
name: rake
|
49
|
-
requirement: &
|
49
|
+
requirement: &70395740 !ruby/object:Gem::Requirement
|
50
50
|
none: false
|
51
51
|
requirements:
|
52
52
|
- - ~>
|
@@ -54,10 +54,10 @@ dependencies:
|
|
54
54
|
version: 10.0.3
|
55
55
|
type: :development
|
56
56
|
prerelease: false
|
57
|
-
version_requirements: *
|
57
|
+
version_requirements: *70395740
|
58
58
|
- !ruby/object:Gem::Dependency
|
59
59
|
name: rspec
|
60
|
-
requirement: &
|
60
|
+
requirement: &70395170 !ruby/object:Gem::Requirement
|
61
61
|
none: false
|
62
62
|
requirements:
|
63
63
|
- - ~>
|
@@ -65,9 +65,9 @@ dependencies:
|
|
65
65
|
version: 2.0.1
|
66
66
|
type: :development
|
67
67
|
prerelease: false
|
68
|
-
version_requirements: *
|
68
|
+
version_requirements: *70395170
|
69
69
|
description: ftpd is a pure Ruby FTP server library. It supports implicit and explicit
|
70
|
-
TLS,
|
70
|
+
TLS, suitlble for use by a program such as a test fixture or FTP daemon.
|
71
71
|
email: wconrad@yagni.com
|
72
72
|
executables: []
|
73
73
|
extensions: []
|
@@ -81,6 +81,7 @@ files:
|
|
81
81
|
- README.md
|
82
82
|
- Rakefile
|
83
83
|
- VERSION
|
84
|
+
- examples/example.rb
|
84
85
|
- features/command_errors.feature
|
85
86
|
- features/delete.feature
|
86
87
|
- features/directory_navigation.feature
|
@@ -128,12 +129,10 @@ files:
|
|
128
129
|
- ftpd.gemspec
|
129
130
|
- insecure-test-cert.pem
|
130
131
|
- lib/ftpd.rb
|
131
|
-
- lib/ftpd/
|
132
|
-
- lib/ftpd/
|
133
|
-
- lib/ftpd/
|
134
|
-
- lib/ftpd/
|
135
|
-
- lib/ftpd/TempDir.rb
|
136
|
-
- lib/ftpd/q.rb
|
132
|
+
- lib/ftpd/ftp_server.rb
|
133
|
+
- lib/ftpd/server.rb
|
134
|
+
- lib/ftpd/temp_dir.rb
|
135
|
+
- lib/ftpd/tls_server.rb
|
137
136
|
- rake_tasks/cucumber.rake
|
138
137
|
- rake_tasks/jeweler.rake
|
139
138
|
homepage: http://github.com/wconrad/ftpd
|
data/lib/ftpd/FakeFtpServer.rb
DELETED
@@ -1,736 +0,0 @@
|
|
1
|
-
#!/usr/bin/env ruby
|
2
|
-
|
3
|
-
require 'fileutils'
|
4
|
-
require 'openssl'
|
5
|
-
require 'pathname'
|
6
|
-
require File.expand_path('FakeTlsServer', File.dirname(__FILE__))
|
7
|
-
require File.expand_path('TempDir', File.dirname(__FILE__))
|
8
|
-
require File.expand_path('q', File.dirname(__FILE__))
|
9
|
-
|
10
|
-
class FakeFtpServer < FakeTlsServer
|
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
|
-
|
666
|
-
class Scaffold
|
667
|
-
|
668
|
-
def initialize
|
669
|
-
@data_dir = TempDir.new
|
670
|
-
create_files
|
671
|
-
@server = FakeFtpServer.new(@data_dir.path)
|
672
|
-
set_credentials
|
673
|
-
display_connection_info
|
674
|
-
create_connection_script
|
675
|
-
end
|
676
|
-
|
677
|
-
def run
|
678
|
-
wait_until_stopped
|
679
|
-
end
|
680
|
-
|
681
|
-
private
|
682
|
-
|
683
|
-
HOST = 'localhost'
|
684
|
-
|
685
|
-
def create_files
|
686
|
-
[
|
687
|
-
'README',
|
688
|
-
'outgoing/getme',
|
689
|
-
].each do |path|
|
690
|
-
base_name = File.basename(path)
|
691
|
-
dir_name = File.dirname(path)
|
692
|
-
dir_path = File.join(@data_dir.path, dir_name)
|
693
|
-
file_path = File.join(dir_path, base_name)
|
694
|
-
FileUtils.mkdir_p(dir_path)
|
695
|
-
File.open(file_path, 'w') do |file|
|
696
|
-
file.puts "Contents of #{path}"
|
697
|
-
end
|
698
|
-
end
|
699
|
-
end
|
700
|
-
|
701
|
-
def set_credentials
|
702
|
-
@server.user = ENV['LOGNAME']
|
703
|
-
@server.password = ''
|
704
|
-
end
|
705
|
-
|
706
|
-
def display_connection_info
|
707
|
-
puts "Host: #{HOST}"
|
708
|
-
puts "Port: #{@server.port}"
|
709
|
-
puts "User: #{@server.user}"
|
710
|
-
puts "Pass: #{@server.password}"
|
711
|
-
puts "Directory: #{@data_dir.path}"
|
712
|
-
end
|
713
|
-
|
714
|
-
def create_connection_script
|
715
|
-
command_path = '/tmp/connect_to_fake_ftp_server.sh'
|
716
|
-
File.open(command_path, 'w') do |file|
|
717
|
-
file.puts "#!/bin/bash"
|
718
|
-
file.puts "ftp $FTP_ARGS #{HOST} #{@server.port}"
|
719
|
-
end
|
720
|
-
system("chmod +x #{command_path}")
|
721
|
-
puts "Connection script written to #{command_path}"
|
722
|
-
end
|
723
|
-
|
724
|
-
def wait_until_stopped
|
725
|
-
puts "FTP server started. Press ENTER or c-C to stop it"
|
726
|
-
$stdout.flush
|
727
|
-
begin
|
728
|
-
gets
|
729
|
-
rescue Interrupt
|
730
|
-
puts "Interrupt"
|
731
|
-
end
|
732
|
-
end
|
733
|
-
|
734
|
-
end
|
735
|
-
|
736
|
-
Scaffold.new.run if $0 == __FILE__
|