fake_ftp 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/.codeclimate.yml +16 -0
  3. data/.gitignore +1 -0
  4. data/.rspec +2 -0
  5. data/.rubocop.yml +27 -0
  6. data/.rubocop_todo.yml +7 -0
  7. data/.simplecov +6 -0
  8. data/.travis.yml +27 -2
  9. data/CHANGELOG.md +136 -0
  10. data/Gemfile +9 -7
  11. data/Guardfile +5 -4
  12. data/LICENSE.md +20 -0
  13. data/README.md +30 -63
  14. data/Rakefile +12 -7
  15. data/fake_ftp.gemspec +16 -17
  16. data/lib/fake_ftp.rb +3 -2
  17. data/lib/fake_ftp/file.rb +11 -5
  18. data/lib/fake_ftp/server.rb +138 -261
  19. data/lib/fake_ftp/server_commands.rb +6 -0
  20. data/lib/fake_ftp/server_commands/acct.rb +11 -0
  21. data/lib/fake_ftp/server_commands/cdup.rb +11 -0
  22. data/lib/fake_ftp/server_commands/cwd.rb +13 -0
  23. data/lib/fake_ftp/server_commands/dele.rb +25 -0
  24. data/lib/fake_ftp/server_commands/list.rb +39 -0
  25. data/lib/fake_ftp/server_commands/mdtm.rb +17 -0
  26. data/lib/fake_ftp/server_commands/mkd.rb +11 -0
  27. data/lib/fake_ftp/server_commands/nlst.rb +32 -0
  28. data/lib/fake_ftp/server_commands/pass.rb +11 -0
  29. data/lib/fake_ftp/server_commands/pasv.rb +15 -0
  30. data/lib/fake_ftp/server_commands/port.rb +23 -0
  31. data/lib/fake_ftp/server_commands/pwd.rb +11 -0
  32. data/lib/fake_ftp/server_commands/quit.rb +13 -0
  33. data/lib/fake_ftp/server_commands/retr.rb +32 -0
  34. data/lib/fake_ftp/server_commands/rnfr.rb +18 -0
  35. data/lib/fake_ftp/server_commands/rnto.rb +22 -0
  36. data/lib/fake_ftp/server_commands/site.rb +11 -0
  37. data/lib/fake_ftp/server_commands/size.rb +11 -0
  38. data/lib/fake_ftp/server_commands/stor.rb +30 -0
  39. data/lib/fake_ftp/server_commands/type.rb +18 -0
  40. data/lib/fake_ftp/server_commands/user.rb +12 -0
  41. data/lib/fake_ftp/server_commands/wat.rb +33 -0
  42. data/lib/fake_ftp/version.rb +3 -1
  43. data/spec/functional/server_spec.rb +459 -358
  44. data/spec/integration/server_spec.rb +39 -29
  45. data/spec/models/fake_ftp/file_spec.rb +22 -21
  46. data/spec/models/fake_ftp/server_spec.rb +33 -32
  47. data/spec/spec_helper.rb +98 -14
  48. metadata +32 -3
data/Rakefile CHANGED
@@ -1,10 +1,15 @@
1
- require 'bundler'
2
- Bundler::GemHelper.install_tasks
1
+ # frozen_string_literal: true
3
2
 
4
- require 'rspec/core'
5
- require 'rspec/core/rake_task'
3
+ begin
4
+ require 'bundler'
5
+ require 'rspec/core/rake_task'
6
+ require 'rubocop/rake_task'
7
+ rescue LoadError => e
8
+ warn e
9
+ end
6
10
 
7
- RSpec::Core::RakeTask.new('spec')
11
+ Bundler::GemHelper.install_tasks if defined?(Bundler)
12
+ RSpec::Core::RakeTask.new if defined?(RSpec)
13
+ RuboCop::RakeTask.new if defined?(RuboCop)
8
14
 
9
- # If you want to make this the default task
10
- task :default => :spec
15
+ task default: %i[rubocop spec]
@@ -1,22 +1,21 @@
1
1
  # -*- encoding: utf-8 -*-
2
- $:.push File.expand_path("../lib", __FILE__)
3
- require "fake_ftp/version"
2
+ # frozen_string_literal: true
3
+
4
+ $LOAD_PATH.unshift(File.expand_path('../lib', __FILE__))
5
+ require 'fake_ftp/version'
4
6
 
5
7
  Gem::Specification.new do |s|
6
- s.name = "fake_ftp"
7
- s.version = FakeFtp::VERSION
8
- s.platform = Gem::Platform::RUBY
9
- s.authors = ["Colin Shield", "Eric Saxby"]
10
- s.email = ["sax+github@livinginthepast.org"]
11
- s.homepage = "http://rubygems.org/gems/fake_ftp"
12
- s.summary = %q{Creates a fake FTP server for use in testing}
13
- s.description = %q{Testing FTP? Use this!}
14
- s.license = 'MIT'
8
+ s.name = 'fake_ftp'
9
+ s.version = FakeFtp::VERSION
10
+ s.platform = Gem::Platform::RUBY
11
+ s.authors = ['Colin Shield', 'Eric Saxby']
12
+ s.email = ['sax+github@livinginthepast.org']
13
+ s.homepage = 'http://rubygems.org/gems/fake_ftp'
14
+ s.summary = 'Creates a fake FTP server for use in testing'
15
+ s.description = 'Testing FTP? Use this!'
16
+ s.license = 'MIT'
15
17
 
16
- s.required_rubygems_version = ">= 1.3.6"
17
-
18
- s.files = `git ls-files`.split("\n")
19
- s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
20
- s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
21
- s.require_paths = ["lib"]
18
+ s.files = `git ls-files -z`.split("\0")
19
+ s.test_files = `git ls-files -z -- spec/*`.split("\0")
20
+ s.require_paths = %w[lib]
22
21
  end
@@ -1,5 +1,6 @@
1
- require 'fake_ftp/server'
2
- require 'fake_ftp/file'
1
+ # frozen_string_literal: true
3
2
 
4
3
  module FakeFtp
4
+ autoload :Server, 'fake_ftp/server'
5
+ autoload :File, 'fake_ftp/file'
5
6
  end
@@ -1,23 +1,29 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module FakeFtp
2
4
  class File
3
- attr_accessor :bytes, :name, :last_modified_time
5
+ attr_accessor :bytes, :data, :name, :last_modified_time
4
6
  attr_writer :type
5
- attr_accessor :data
6
7
  attr_reader :created
7
8
 
8
- def initialize(name = nil, data = nil, type = nil, last_modified_time = Time.now)
9
+ def initialize(name = nil, data = nil, type = nil,
10
+ last_modified_time = Time.now)
9
11
  @created = Time.now
10
12
  @name = name
11
13
  @data = data
12
- # FIXME this is far too ambiguous. args should not mean different
14
+ # FIXME: this is far too ambiguous. args should not mean different
13
15
  # things in different contexts.
14
- data_is_bytes = (data.nil? || Integer === data)
16
+ data_is_bytes = (data.nil? || data.is_a?(Integer))
15
17
  @bytes = data_is_bytes ? data : data.to_s.length
16
18
  @data = data_is_bytes ? nil : data
17
19
  @type = type
18
20
  @last_modified_time = last_modified_time.utc
19
21
  end
20
22
 
23
+ def basename
24
+ ::File.basename(@name)
25
+ end
26
+
21
27
  def data=(data)
22
28
  @data = data
23
29
  @bytes = @data.nil? ? nil : data.length
@@ -1,63 +1,67 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'socket'
2
4
  require 'thread'
3
5
  require 'timeout'
4
6
 
5
7
  module FakeFtp
6
8
  class Server
9
+ attr_accessor :client, :command_state, :data_server, :passive_port
10
+ attr_accessor :port, :store, :workdir
7
11
 
8
- attr_accessor :port, :passive_port
9
- attr_reader :mode, :path
10
-
11
- CMDS = %w(
12
- acct
13
- cwd
14
- cdup
15
- dele
16
- list
17
- mdtm
18
- mkd
19
- nlst
20
- pass
21
- pasv
22
- port
23
- pwd
24
- quit
25
- size
26
- stor
27
- retr
28
- rnfr
29
- rnto
30
- type
31
- user
32
- )
33
- LNBK = "\r\n"
12
+ alias path workdir
34
13
 
35
14
  def initialize(control_port = 21, data_port = nil, options = {})
36
- self.port = control_port
37
- self.passive_port = data_port
38
- raise(Errno::EADDRINUSE, "#{port}") if !control_port.zero? && is_running?
39
- raise(Errno::EADDRINUSE, "#{passive_port}") if passive_port && !passive_port.zero? && is_running?(passive_port)
40
- @connection = nil
15
+ @port = control_port
16
+ @passive_port = data_port
17
+ @store = {}
18
+ @workdir = '/pub'
41
19
  @options = options
42
- @files = []
43
- @mode = :active
44
- @path = "/pub"
20
+ @command_state = {}
21
+
22
+ @connection = nil
23
+ @data_server = nil
24
+ @server = nil
25
+ @client = nil
26
+
27
+ raise Errno::EADDRINUSE, port.to_s if !control_port.zero? && running?
28
+
29
+ if passive_port && !passive_port.zero? && running?(passive_port)
30
+ raise Errno::EADDRINUSE, passive_port.to_s
31
+ end
32
+
33
+ self.mode = options.fetch(:mode, :active)
34
+ self.absolute = options.fetch(:absolute, false)
45
35
  end
46
36
 
47
37
  def files
48
- @files.map(&:name)
38
+ @store.values.map do |f|
39
+ if absolute?
40
+ abspath(f.name)
41
+ else
42
+ f.name
43
+ end
44
+ end
49
45
  end
50
46
 
51
47
  def file(name)
52
- @files.detect { |file| file.name == name }
48
+ @store.values.detect do |f|
49
+ if absolute?
50
+ abspath(f.name) == name
51
+ else
52
+ f.name == name
53
+ end
54
+ end
53
55
  end
54
56
 
55
57
  def reset
56
- @files.clear
58
+ @store.clear
57
59
  end
58
60
 
59
61
  def add_file(filename, data, last_modified_time = Time.now)
60
- @files << FakeFtp::File.new(::File.basename(filename.to_s), data, @mode, last_modified_time)
62
+ @store[abspath(filename)] = FakeFtp::File.new(
63
+ filename.to_s, data, options[:mode], last_modified_time
64
+ )
61
65
  end
62
66
 
63
67
  def start
@@ -66,20 +70,36 @@ module FakeFtp
66
70
  @port = @server.addr[1]
67
71
  @thread = Thread.new do
68
72
  while @started
69
- @client = @server.accept rescue nil
70
- if @client
71
- respond_with('220 Can has FTP?')
72
- @connection = Thread.new(@client) do |socket|
73
- while @started && !socket.nil? && !socket.closed?
74
- input = socket.gets rescue nil
75
- respond_with parse(input) if input
76
- end
77
- unless @client.nil?
78
- @client.close unless @client.closed?
79
- @client = nil
73
+ debug('enter client loop')
74
+ @client = begin
75
+ @server.accept
76
+ rescue => e
77
+ debug("error on accept: #{e}")
78
+ nil
79
+ end
80
+ next unless @client
81
+ respond_with('220 Can has FTP?')
82
+ @connection = Thread.new(@client) do |socket|
83
+ debug('enter request thread')
84
+ while @started && !socket.nil? && !socket.closed?
85
+ input = begin
86
+ socket.gets
87
+ rescue
88
+ debug("error on socket.gets: #{e}")
89
+ nil
90
+ end
91
+ if input
92
+ debug("server client raw: <- #{input.inspect}")
93
+ respond_with(handle_request(input))
80
94
  end
81
95
  end
96
+ unless @client.nil?
97
+ @client.close unless @client.closed?
98
+ @client = nil
99
+ end
100
+ debug('leave request thread')
82
101
  end
102
+ debug('leave client loop')
83
103
  end
84
104
  unless @server.nil?
85
105
  @server.close unless @server.closed?
@@ -87,258 +107,110 @@ module FakeFtp
87
107
  end
88
108
  end
89
109
 
90
- if passive_port
91
- @data_server = ::TCPServer.new('127.0.0.1', passive_port)
92
- @passive_port = @data_server.addr[1]
93
- end
110
+ return unless passive_port
111
+ @data_server = ::TCPServer.new('127.0.0.1', passive_port)
112
+ @passive_port = @data_server.addr[1]
94
113
  end
95
114
 
96
115
  def stop
97
116
  @started = false
98
- @client.close if @client
99
- @server.close if @server
117
+ @client&.close
118
+ @server&.close
100
119
  @server = nil
101
- @data_server.close if @data_server
120
+ @data_server&.close
102
121
  @data_server = nil
103
122
  end
104
123
 
105
- def is_running?(tcp_port = nil)
106
- tcp_port.nil? ? port_is_open?(port) : port_is_open?(tcp_port)
107
- end
108
-
109
- private
110
-
111
- def respond_with(stuff)
112
- @client.print stuff << LNBK unless stuff.nil? or @client.nil? or @client.closed?
113
- end
114
-
115
- def parse(request)
116
- return if request.nil?
117
- puts request if @options[:debug]
118
- command = request[0, 4].downcase.strip
119
- contents = request.split
120
- message = contents[1..contents.length]
121
- case command
122
- when *CMDS
123
- __send__ "_#{command}", *message
124
- else
125
- '500 Unknown command'
126
- end
127
- end
128
-
129
-
130
- ## FTP commands
131
- #
132
- # Methods are prefixed with an underscore to avoid conflicts with internal server
133
- # methods. Methods map 1:1 to FTP command words.
134
- #
135
- def _acct(*args)
136
- '230 WHATEVER!'
137
- end
138
-
139
- def _cwd(*args)
140
- @path = args[0]
141
- @path = "/#{path}" if path[0].chr != "/"
142
- '250 OK!'
143
- end
144
-
145
- def _cdup(*args)
146
- '250 OK!'
147
- end
148
-
149
- def _list(*args)
150
- respond_with('425 Ain\'t no data port!') && return if active? && @active_connection.nil?
151
-
152
- respond_with('150 Listing status ok, about to open data connection')
153
- data_client = active? ? @active_connection : @data_server.accept
154
-
155
- wildcards = build_wildcards(args)
156
- files = matching_files(@files, wildcards)
157
-
158
- files = files.map do |f|
159
- "-rw-r--r--\t1\towner\tgroup\t#{f.bytes}\t#{f.created.strftime('%b %d %H:%M')}\t#{f.name}\n"
160
- end
161
- data_client.write(files.join)
162
- data_client.close
163
- @active_connection = nil
164
-
165
- '226 List information transferred'
166
- end
167
-
168
- def _mdtm(filename = '', local = false)
169
- respond_with('501 No filename given') && return if filename.empty?
170
- server_file = file(filename)
171
- respond_with('550 File not found') && return if server_file.nil?
172
-
173
- respond_with("213 #{server_file.last_modified_time.strftime("%Y%m%d%H%M%S")}")
124
+ def running?(tcp_port = nil)
125
+ return port_is_open?(port) if tcp_port.nil?
126
+ port_is_open?(tcp_port)
174
127
  end
175
128
 
176
- def _nlst(*args)
177
- respond_with('425 Ain\'t no data port!') && return if active? && @active_connection.nil?
178
-
179
- respond_with('150 Listing status ok, about to open data connection')
180
- data_client = active? ? @active_connection : @data_server.accept
129
+ alias is_running? running?
181
130
 
182
- wildcards = build_wildcards(args)
183
- files = matching_files(@files, wildcards)
184
-
185
- files = files.map do |file|
186
- "#{file.name}\n"
131
+ def mode=(value)
132
+ unless %i[active passive].include?(value)
133
+ raise ArgumentError, "invalid mode #{value.inspect}"
187
134
  end
188
-
189
- data_client.write(files.join)
190
- data_client.close
191
- @active_connection = nil
192
-
193
- '226 List information transferred'
135
+ options[:mode] = value
194
136
  end
195
137
 
196
- def _pass(*args)
197
- '230 logged in'
138
+ def mode
139
+ options[:mode]
198
140
  end
199
141
 
200
- def _pasv(*args)
201
- if passive_port
202
- @mode = :passive
203
- p1 = (passive_port / 256).to_i
204
- p2 = passive_port % 256
205
- "227 Entering Passive Mode (127,0,0,1,#{p1},#{p2})"
206
- else
207
- '502 Aww hell no, use Active'
208
- end
142
+ def absolute?
143
+ options[:absolute]
209
144
  end
210
145
 
211
- def _port(remote = '')
212
- remote = remote.split(',')
213
- remote_port = remote[4].to_i * 256 + remote[5].to_i
214
- unless @active_connection.nil?
215
- @active_connection.close
216
- @active_connection = nil
146
+ def absolute=(value)
147
+ unless [true, false].include?(value)
148
+ raise ArgumentError, "invalid absolute #{value}"
217
149
  end
218
- @mode = :active
219
- @active_connection = ::TCPSocket.open('127.0.0.1', remote_port)
220
- '200 Okay'
150
+ options[:absolute] = value
221
151
  end
222
152
 
223
- def _pwd(*args)
224
- "257 \"#{path}\" is current directory"
225
- end
153
+ attr_reader :options
154
+ private :options
226
155
 
227
- def _quit(*args)
228
- respond_with '221 OMG bye!'
229
- @client.close if @client
230
- @client = nil
156
+ def abspath(filename)
157
+ return filename if filename.start_with?('/')
158
+ [@workdir.to_s, filename].join('/').gsub('//', '/')
231
159
  end
232
160
 
233
- def _retr(filename = '')
234
- respond_with('501 No filename given') if filename.empty?
235
-
236
- file = file(::File.basename(filename.to_s))
237
- return respond_with('550 File not found') if file.nil?
238
-
239
- respond_with('425 Ain\'t no data port!') && return if active? && @active_connection.nil?
240
-
241
- respond_with('150 File status ok, about to open data connection')
242
- data_client = active? ? @active_connection : @data_server.accept
243
-
244
- data_client.write(file.data)
245
-
246
- data_client.close
247
- @active_connection = nil
248
- '226 File transferred'
249
- end
250
-
251
- def _rnfr(rename_from='')
252
- return '501 Send path name.' if rename_from.nil? || rename_from.size < 1
253
-
254
- @rename_from = rename_from
255
- '350 Send RNTO to complete rename.'
256
- end
257
-
258
- def _rnto(rename_to='')
259
- return '501 Send path name.' if rename_to.nil? || rename_to.size < 1
260
-
261
- return '503 Send RNFR first.' unless @rename_from
262
-
263
- if file = file(@rename_from)
264
- file.name = rename_to
265
- @rename_from = nil
266
- '250 Path renamed.'
267
- else
268
- @rename_from = nil
269
- '550 File not found.'
270
- end
271
- end
272
-
273
- def _size(filename)
274
- file_size = file(filename).bytes
275
- respond_with("213 #{file_size}")
276
- end
277
-
278
- def _stor(filename = '')
279
- respond_with('425 Ain\'t no data port!') && return if active? && @active_connection.nil?
280
-
281
- respond_with('125 Do it!')
282
- data_client = active? ? @active_connection : @data_server.accept
283
-
284
- data = data_client.read(nil).chomp
285
- file = FakeFtp::File.new(::File.basename(filename.to_s), data, @mode)
286
- @files << file
287
-
288
- data_client.close
289
- @active_connection = nil
290
- '226 Did it!'
161
+ def respond_with(stuff)
162
+ return if stuff.nil? || @client.nil? || @client.closed?
163
+ debug("server client raw: -> #{stuff.inspect}")
164
+ @client.print(stuff + "\r\n")
291
165
  end
292
166
 
293
- def _dele(filename = '')
294
- files_to_delete = @files.select{ |file| file.name == filename }
295
- return '550 Delete operation failed.' if files_to_delete.count == 0
296
-
297
- @files = @files - files_to_delete
167
+ private def handle_request(request)
168
+ return if request.nil?
169
+ debug("raw request: #{request.inspect}")
170
+ command = request[0, 4].downcase.strip
171
+ contents = request.split
172
+ message = contents[1..contents.length]
298
173
 
299
- '250 Delete operation successful.'
174
+ inst = load_command_instance(command)
175
+ return "500 Unknown command #{command.inspect}" if inst.nil?
176
+ debug(
177
+ "running command #{command.inspect} " \
178
+ "#{inst.class.name}#run(*#{message.inspect})"
179
+ )
180
+ inst.run(*([self] + message))
300
181
  end
301
182
 
302
- def _type(type = 'A')
303
- case type.to_s
304
- when 'A'
305
- '200 Type set to A.'
306
- when 'I'
307
- '200 Type set to I.'
308
- else
309
- '504 We don\'t allow those'
183
+ private def load_command_instance(command)
184
+ require "fake_ftp/server_commands/#{command}"
185
+ FakeFtp::ServerCommands.constants.each do |const_name|
186
+ next unless const_name.to_s.downcase == command
187
+ return FakeFtp::ServerCommands.const_get(const_name).new
310
188
  end
311
- end
312
-
313
- def _user(name = '')
314
- (name.to_s == 'anonymous') ? '230 logged in' : '331 send your password'
315
- end
316
-
317
- def _mkd(directory)
318
- "257 OK!"
189
+ nil
190
+ rescue LoadError => e
191
+ debug("failed to require #{command.inspect} class: #{e}")
192
+ nil
319
193
  end
320
194
 
321
195
  def active?
322
- @mode == :active
196
+ options[:mode] == :active
323
197
  end
324
198
 
325
- private
326
-
327
- def port_is_open?(port)
199
+ private def port_is_open?(port)
328
200
  begin
329
- Timeout::timeout(1) do
201
+ Timeout.timeout(1) do
330
202
  begin
331
- s = TCPSocket.new("127.0.0.1", port)
332
- s.close
203
+ TCPSocket.new('127.0.0.1', port).close
333
204
  return true
334
205
  rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH
335
206
  return false
336
207
  end
337
208
  end
338
- rescue Timeout::Error
209
+ rescue Timeout::Error => e
210
+ debug("timeout while checking port #{port}: #{e}")
339
211
  end
340
212
 
341
- return false
213
+ false
342
214
  end
343
215
 
344
216
  def build_wildcards(args)
@@ -350,14 +222,19 @@ module FakeFtp
350
222
  wildcards
351
223
  end
352
224
 
353
- def matching_files(files, wildcards)
354
- if not wildcards.empty?
355
- files.select do |f|
225
+ def matching_files(wildcards)
226
+ if !wildcards.empty?
227
+ @store.values.select do |f|
356
228
  wildcards.any? { |wildcard| f.name =~ /#{wildcard}/ }
357
229
  end
358
230
  else
359
- files
231
+ @store.values
360
232
  end
361
233
  end
234
+
235
+ def debug(msg)
236
+ return unless options[:debug]
237
+ $stderr.puts("DEBUG:fake_ftp:#{msg}")
238
+ end
362
239
  end
363
240
  end