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/temp_dir.rb
CHANGED
@@ -1,16 +1,15 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
module Ftpd
|
2
|
+
module TempDir
|
3
3
|
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
Dir.rmdir path if File.exists?(path)
|
4
|
+
def make
|
5
|
+
Dir.mktmpdir.tap do |path|
|
6
|
+
at_exit do
|
7
|
+
FileUtils.rm_rf path
|
8
|
+
Dir.rmdir path if File.exists?(path)
|
9
|
+
end
|
11
10
|
end
|
12
11
|
end
|
13
|
-
|
14
|
-
module_function :make
|
12
|
+
module_function :make
|
15
13
|
|
14
|
+
end
|
16
15
|
end
|
data/lib/ftpd/tls_server.rb
CHANGED
@@ -1,42 +1,48 @@
|
|
1
|
-
require 'openssl'
|
2
|
-
require 'ftpd/server'
|
3
|
-
|
4
1
|
module Ftpd
|
5
2
|
class TlsServer < Server
|
6
3
|
|
4
|
+
attr_accessor :tls
|
5
|
+
attr_accessor :certfile_path
|
6
|
+
|
7
7
|
def initialize
|
8
|
-
@ssl_context = make_ssl_context
|
9
8
|
super
|
9
|
+
@tls = :off
|
10
|
+
if tls_enabled?
|
11
|
+
unless @certfile_path
|
12
|
+
raise ArgumentError, ":certfile required if tls enabled"
|
13
|
+
end
|
14
|
+
end
|
10
15
|
end
|
11
16
|
|
12
17
|
private
|
13
18
|
|
14
19
|
def make_server_socket
|
15
|
-
|
16
|
-
|
17
|
-
|
20
|
+
socket = super
|
21
|
+
if tls_enabled?
|
22
|
+
socket = OpenSSL::SSL::SSLServer.new(socket, ssl_context);
|
23
|
+
socket.start_immediately = false
|
24
|
+
end
|
25
|
+
socket
|
18
26
|
end
|
19
27
|
|
20
28
|
def accept
|
21
29
|
socket = @server_socket.accept
|
22
|
-
|
30
|
+
if tls_enabled?
|
31
|
+
add_tls_methods_to_socket(socket)
|
32
|
+
end
|
23
33
|
socket
|
24
34
|
end
|
25
35
|
|
26
|
-
def
|
36
|
+
def ssl_context
|
27
37
|
context = OpenSSL::SSL::SSLContext.new
|
28
|
-
File.open(certfile_path) do |certfile|
|
38
|
+
File.open(@certfile_path) do |certfile|
|
29
39
|
context.cert = OpenSSL::X509::Certificate.new(certfile)
|
30
40
|
certfile.rewind
|
31
41
|
context.key = OpenSSL::PKey::RSA.new(certfile)
|
32
42
|
end
|
33
43
|
context
|
34
44
|
end
|
35
|
-
|
36
|
-
def certfile_path
|
37
|
-
File.expand_path('../../insecure-test-cert.pem',
|
38
|
-
File.dirname(__FILE__))
|
39
|
-
end
|
45
|
+
memoize :ssl_context
|
40
46
|
|
41
47
|
def add_tls_methods_to_socket(socket)
|
42
48
|
context = @ssl_context
|
@@ -53,5 +59,11 @@ module Ftpd
|
|
53
59
|
end
|
54
60
|
end
|
55
61
|
|
62
|
+
private
|
63
|
+
|
64
|
+
def tls_enabled?
|
65
|
+
@tls != :off
|
66
|
+
end
|
67
|
+
|
56
68
|
end
|
57
69
|
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module Ftpd
|
2
|
+
|
3
|
+
# This module provides an easy interface to ExceptionTranslator.
|
4
|
+
|
5
|
+
module TranslateExceptions
|
6
|
+
|
7
|
+
include Memoizer
|
8
|
+
|
9
|
+
def self.included(includer)
|
10
|
+
includer.extend ClassMethods
|
11
|
+
end
|
12
|
+
|
13
|
+
module ClassMethods
|
14
|
+
|
15
|
+
# Cause the named method to translate exceptions.
|
16
|
+
|
17
|
+
def translate_exceptions(method_name)
|
18
|
+
original_method = instance_method(method_name)
|
19
|
+
define_method method_name do |*args|
|
20
|
+
exception_translator.translate_exceptions do
|
21
|
+
original_method.bind(self).call *args
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
|
28
|
+
# Add exception class e to the list of exceptions to be
|
29
|
+
# translated.
|
30
|
+
|
31
|
+
def translate_exception(e)
|
32
|
+
exception_translator.register_exception e
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def exception_translator
|
38
|
+
ExceptionTranslator.new
|
39
|
+
end
|
40
|
+
memoize :exception_translator
|
41
|
+
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
data/rake_tasks/cucumber.rake
CHANGED
@@ -1,7 +1,9 @@
|
|
1
1
|
require 'cucumber/rake/task'
|
2
2
|
|
3
|
-
Cucumber::Rake::Task.new
|
3
|
+
Cucumber::Rake::Task.new 'test:features' do |t|
|
4
4
|
t.fork = true
|
5
|
+
t.cucumber_opts = '--format progress'
|
5
6
|
end
|
6
7
|
|
7
|
-
task :cucumber => [:features]
|
8
|
+
task 'test:cucumber' => ['test:features']
|
9
|
+
task 'cucumber' => ['test:features']
|
@@ -0,0 +1 @@
|
|
1
|
+
task :default => [:test]
|
@@ -0,0 +1,37 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'eventmachine'
|
4
|
+
|
5
|
+
module EchoServer
|
6
|
+
|
7
|
+
def post_init
|
8
|
+
p 'post_init'
|
9
|
+
end
|
10
|
+
|
11
|
+
def connection_completed
|
12
|
+
p 'connection_completed'
|
13
|
+
end
|
14
|
+
|
15
|
+
def receive_data data
|
16
|
+
send_data ">>>you sent: #{data}"
|
17
|
+
close_connection if data =~ /quit/i
|
18
|
+
end
|
19
|
+
|
20
|
+
def unbind
|
21
|
+
p 'unbind'
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def get_port_for_fd(fd)
|
26
|
+
sockname = EM.get_sockname(fd)
|
27
|
+
port, host = Socket.unpack_sockaddr_in(sockname)
|
28
|
+
port
|
29
|
+
end
|
30
|
+
|
31
|
+
# Note that this will block current thread.
|
32
|
+
EventMachine.run {
|
33
|
+
fd = EventMachine.start_server "127.0.0.1", 0, EchoServer do |e|
|
34
|
+
p e
|
35
|
+
end
|
36
|
+
p get_port_for_fd(fd)
|
37
|
+
}
|
@@ -0,0 +1,239 @@
|
|
1
|
+
require File.expand_path('spec_helper', File.dirname(__FILE__))
|
2
|
+
|
3
|
+
module Ftpd
|
4
|
+
describe DiskFileSystem do
|
5
|
+
|
6
|
+
let(:data_dir) {Ftpd::TempDir.make}
|
7
|
+
let(:disk_file_system) {DiskFileSystem.new(data_dir)}
|
8
|
+
let(:missing_file_error) do
|
9
|
+
[Ftpd::FileSystemError, /No such file or directory/]
|
10
|
+
end
|
11
|
+
let(:is_a_directory_error) do
|
12
|
+
[Ftpd::FileSystemError, /Is a directory/]
|
13
|
+
end
|
14
|
+
let(:missing_path) {'missing_path'}
|
15
|
+
|
16
|
+
def data_path(path)
|
17
|
+
File.join(data_dir, path)
|
18
|
+
end
|
19
|
+
|
20
|
+
def make_directory(path)
|
21
|
+
Dir.mkdir data_path(path)
|
22
|
+
end
|
23
|
+
|
24
|
+
def write_file(path)
|
25
|
+
File.open(data_path(path), 'wb') do |file|
|
26
|
+
file.write canned_contents(path)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def read_file(path)
|
31
|
+
File.open(data_path(path), 'rb', &:read)
|
32
|
+
end
|
33
|
+
|
34
|
+
def exists?(path)
|
35
|
+
File.exists?(data_path(path))
|
36
|
+
end
|
37
|
+
|
38
|
+
def canned_contents(path)
|
39
|
+
"Contents of #{path}"
|
40
|
+
end
|
41
|
+
|
42
|
+
before(:each) do
|
43
|
+
write_file 'file'
|
44
|
+
make_directory 'dir'
|
45
|
+
write_file 'dir/file_in_dir'
|
46
|
+
make_directory 'unwritable_dir'
|
47
|
+
write_file 'unwritable_dir/file'
|
48
|
+
end
|
49
|
+
|
50
|
+
describe '#accessible?' do
|
51
|
+
|
52
|
+
context '(within tree)' do
|
53
|
+
specify do
|
54
|
+
disk_file_system.accessible?('file').should be_true
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
context '(outside tree)' do
|
59
|
+
specify do
|
60
|
+
disk_file_system.accessible?('../outside').should be_false
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
65
|
+
|
66
|
+
describe '#exists?' do
|
67
|
+
|
68
|
+
context '(exists)' do
|
69
|
+
specify do
|
70
|
+
disk_file_system.exists?('file').should be_true
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
context '(does not exist)' do
|
75
|
+
specify do
|
76
|
+
disk_file_system.exists?('missing').should be_false
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
81
|
+
|
82
|
+
describe '#directory?' do
|
83
|
+
|
84
|
+
context '(directory)' do
|
85
|
+
specify do
|
86
|
+
disk_file_system.directory?('file').should be_false
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
context '(file)' do
|
91
|
+
specify do
|
92
|
+
disk_file_system.directory?('dir').should be_true
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
end
|
97
|
+
|
98
|
+
describe '#delete' do
|
99
|
+
|
100
|
+
context '(normal)' do
|
101
|
+
specify do
|
102
|
+
disk_file_system.delete('file')
|
103
|
+
exists?('file').should be_false
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
context '(file system error)' do
|
108
|
+
specify do
|
109
|
+
expect {
|
110
|
+
disk_file_system.delete(missing_path)
|
111
|
+
}.to raise_error *missing_file_error
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
end
|
116
|
+
|
117
|
+
describe '#read' do
|
118
|
+
|
119
|
+
context '(normal)' do
|
120
|
+
let(:path) {'file'}
|
121
|
+
specify do
|
122
|
+
disk_file_system.read(path).should == canned_contents(path)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
context '(file system error)' do
|
127
|
+
specify do
|
128
|
+
expect {
|
129
|
+
disk_file_system.read(missing_path)
|
130
|
+
}.to raise_error *missing_file_error
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
end
|
135
|
+
|
136
|
+
describe '#write' do
|
137
|
+
|
138
|
+
let(:contents) {'file contents'}
|
139
|
+
|
140
|
+
context '(normal)' do
|
141
|
+
let(:path) {'file_path'}
|
142
|
+
specify do
|
143
|
+
disk_file_system.write(path, contents)
|
144
|
+
read_file(path).should == contents
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
context '(file system error)' do
|
149
|
+
specify do
|
150
|
+
expect {
|
151
|
+
disk_file_system.write('dir', contents)
|
152
|
+
}.to raise_error *is_a_directory_error
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
end
|
157
|
+
|
158
|
+
describe '#list_short' do
|
159
|
+
|
160
|
+
subject do
|
161
|
+
disk_file_system.list_short(path)
|
162
|
+
end
|
163
|
+
|
164
|
+
shared_examples 'returns short list of root' do
|
165
|
+
it {should =~ /^dir$/}
|
166
|
+
it {should =~ /^file$/}
|
167
|
+
it {should =~ /^unwritable_dir$/}
|
168
|
+
its('lines.to_a.size') {should == 3}
|
169
|
+
end
|
170
|
+
|
171
|
+
context '(root)' do
|
172
|
+
let(:path) {'/'}
|
173
|
+
it_behaves_like 'returns short list of root'
|
174
|
+
end
|
175
|
+
|
176
|
+
context '(empty path)' do
|
177
|
+
let(:path) {''}
|
178
|
+
it_behaves_like 'returns short list of root'
|
179
|
+
end
|
180
|
+
|
181
|
+
context '(specific file)' do
|
182
|
+
let(:path) {'/file'}
|
183
|
+
it {should == "file\n"}
|
184
|
+
end
|
185
|
+
|
186
|
+
context '(specific directory)' do
|
187
|
+
let(:path) {'/dir'}
|
188
|
+
it {should == "file_in_dir\n"}
|
189
|
+
end
|
190
|
+
|
191
|
+
context '(missing directory)' do
|
192
|
+
let(:path) {'/missing/file'}
|
193
|
+
it {should be_empty}
|
194
|
+
end
|
195
|
+
|
196
|
+
end
|
197
|
+
|
198
|
+
describe '#list_long' do
|
199
|
+
|
200
|
+
subject do
|
201
|
+
disk_file_system.list_long(path)
|
202
|
+
end
|
203
|
+
|
204
|
+
shared_examples 'returns long list of root' do
|
205
|
+
it {should =~ /^d.*dir$/}
|
206
|
+
it {should =~ /^-.*file$/}
|
207
|
+
it {should =~ /^d.*unwritable_dir$/}
|
208
|
+
its('lines.to_a.size') {should == 3}
|
209
|
+
end
|
210
|
+
|
211
|
+
context '(root)' do
|
212
|
+
let(:path) {'/'}
|
213
|
+
it_behaves_like 'returns long list of root'
|
214
|
+
end
|
215
|
+
|
216
|
+
context '(empty path)' do
|
217
|
+
let(:path) {''}
|
218
|
+
it_behaves_like 'returns long list of root'
|
219
|
+
end
|
220
|
+
|
221
|
+
context '(specific file)' do
|
222
|
+
let(:path) {'/file'}
|
223
|
+
it {should =~ /^-.*file$/}
|
224
|
+
end
|
225
|
+
|
226
|
+
context '(specific directory)' do
|
227
|
+
let(:path) {'/dir'}
|
228
|
+
it {should =~ /^-.*file_in_dir\n/}
|
229
|
+
end
|
230
|
+
|
231
|
+
context '(missing directory)' do
|
232
|
+
let(:path) {'/missing/file'}
|
233
|
+
it {should be_empty}
|
234
|
+
end
|
235
|
+
|
236
|
+
end
|
237
|
+
|
238
|
+
end
|
239
|
+
end
|