ftpd 2.0.0 → 2.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: da62814ce49fab3ad30c35b6841810728722bc99
4
- data.tar.gz: fd68f590b35708037f0ca1678dd84936dee7d4e1
3
+ metadata.gz: 637e5bfc2505f89b270ab1a8f89df0dfdb02e84c
4
+ data.tar.gz: c5209641735180ea65417e1c7ed9afeea370c946
5
5
  SHA512:
6
- metadata.gz: a84a4a7d0554c71e71816376b7cac3268d0f6250a58d34ffa0a9bc1f5116468b73e36c5f09a333f28b173cf9c4dd9e7427ac5c9f9cb93cf12f63344ae7bfd09e
7
- data.tar.gz: ffd9e7e5477c2afd9301158be878968ed7e601677433f5c8e2b0dfee050106f580026235368a726e05a84da0f92a25369de649ddf19a1fa03ff8b2e7d591ce98
6
+ metadata.gz: 16be5eac5cfe71218a0bd02bb32585ca86f4d241cb6c5ba80b531d8b388a04375f069d1a2f8af323b6a7e452678ddc9239091037b52dae7284f696f04f288e65
7
+ data.tar.gz: 5dfcee331e594fd2d5a8f7b956d81b71a9b2693ecbf5ffb9603cbd0f83aa1e4df945216eb7f403b37067b2851f7efdedc81bf81b17a495493f84f4213f49e487
data/Changelog.md CHANGED
@@ -2,7 +2,11 @@ This is the change log for the main branch of ftpd, which supports
2
2
  Ruby 1.9 and greater. For ruby 1.8.7, please use the latest version
3
3
  before 0.8.0.
4
4
 
5
- ### Development
5
+ ### 2.0.1 2016-11-02
6
+
7
+ * Redact password in log (thanks ukolovda)
8
+
9
+ ### 2.0.0 2016-06-01
6
10
 
7
11
  Breaking changes:
8
12
 
data/Gemfile CHANGED
@@ -4,7 +4,7 @@ gem 'memoizer', '~> 1.0'
4
4
 
5
5
  group :development do
6
6
  gem 'cucumber', '~> 2.0'
7
- gem 'double-bag-ftps', '~> 0.1'
7
+ gem 'double-bag-ftps', '~> 0.1', '>= 0.1.3'
8
8
  gem 'jeweler', '~> 2.0'
9
9
  gem 'rake', '~> 11.1'
10
10
  gem 'redcarpet', '~> 3.1'
data/Gemfile.lock CHANGED
@@ -3,62 +3,63 @@ GEM
3
3
  specs:
4
4
  addressable (2.4.0)
5
5
  builder (3.2.2)
6
- cucumber (2.3.3)
6
+ cucumber (2.4.0)
7
7
  builder (>= 2.1.2)
8
- cucumber-core (~> 1.4.0)
8
+ cucumber-core (~> 1.5.0)
9
9
  cucumber-wire (~> 0.0.1)
10
10
  diff-lcs (>= 1.1.3)
11
- gherkin (~> 3.2.0)
11
+ gherkin (~> 4.0)
12
12
  multi_json (>= 1.7.5, < 2.0)
13
13
  multi_test (>= 0.1.2)
14
- cucumber-core (1.4.0)
15
- gherkin (~> 3.2.0)
14
+ cucumber-core (1.5.0)
15
+ gherkin (~> 4.0)
16
16
  cucumber-wire (0.0.1)
17
17
  descendants_tracker (0.0.4)
18
18
  thread_safe (~> 0.3, >= 0.3.1)
19
19
  diff-lcs (1.2.5)
20
- double-bag-ftps (0.1.2)
20
+ double-bag-ftps (0.1.3)
21
21
  faraday (0.9.2)
22
22
  multipart-post (>= 1.2, < 3)
23
- gherkin (3.2.0)
23
+ gherkin (4.0.0)
24
24
  git (1.3.0)
25
- github_api (0.13.1)
26
- addressable (~> 2.4.0)
27
- descendants_tracker (~> 0.0.4)
25
+ github_api (0.11.3)
26
+ addressable (~> 2.3)
27
+ descendants_tracker (~> 0.0.1)
28
28
  faraday (~> 0.8, < 0.10)
29
- hashie (>= 3.4)
29
+ hashie (>= 1.2)
30
30
  multi_json (>= 1.7.5, < 2.0)
31
+ nokogiri (~> 1.6.0)
31
32
  oauth2
32
- hashie (3.4.3)
33
+ hashie (3.4.6)
33
34
  highline (1.7.8)
34
- jeweler (2.1.1)
35
+ jeweler (2.1.2)
35
36
  builder
36
37
  bundler (>= 1.0)
37
38
  git (>= 1.2.5)
38
- github_api
39
+ github_api (~> 0.11.0)
39
40
  highline (>= 1.6.15)
40
41
  nokogiri (>= 1.5.10)
41
42
  rake
42
43
  rdoc
43
44
  semver
44
45
  json (1.8.3)
45
- jwt (1.5.1)
46
+ jwt (1.5.6)
46
47
  memoizer (1.0.1)
47
- mini_portile2 (2.0.0)
48
- multi_json (1.11.2)
48
+ mini_portile2 (2.1.0)
49
+ multi_json (1.12.1)
49
50
  multi_test (0.1.2)
50
51
  multi_xml (0.5.5)
51
52
  multipart-post (2.0.0)
52
- nokogiri (1.6.7.2)
53
- mini_portile2 (~> 2.0.0.rc2)
54
- oauth2 (1.1.0)
53
+ nokogiri (1.6.8.1)
54
+ mini_portile2 (~> 2.1.0)
55
+ oauth2 (1.2.0)
55
56
  faraday (>= 0.8, < 0.10)
56
- jwt (~> 1.0, < 1.5.2)
57
+ jwt (~> 1.0)
57
58
  multi_json (~> 1.3)
58
59
  multi_xml (~> 0.5)
59
60
  rack (>= 1.2, < 3)
60
- rack (1.6.4)
61
- rake (11.1.2)
61
+ rack (2.0.1)
62
+ rake (11.3.0)
62
63
  rdoc (4.2.2)
63
64
  json (~> 1.4)
64
65
  redcarpet (3.3.4)
@@ -88,7 +89,7 @@ PLATFORMS
88
89
 
89
90
  DEPENDENCIES
90
91
  cucumber (~> 2.0)
91
- double-bag-ftps (~> 0.1)
92
+ double-bag-ftps (~> 0.1, >= 0.1.3)
92
93
  jeweler (~> 2.0)
93
94
  memoizer (~> 1.0)
94
95
  rake (~> 11.1)
@@ -99,4 +100,4 @@ DEPENDENCIES
99
100
  yard (~> 0.8.7)
100
101
 
101
102
  BUNDLED WITH
102
- 1.11.2
103
+ 1.13.1
data/README.md CHANGED
@@ -174,8 +174,8 @@ server.start
174
174
 
175
175
  You can set any of these attributes before starting the server:
176
176
 
177
- # {Ftpd::FtpServer#nat_ip}
178
- # {Ftpd::FtpServer#passive_ports}
177
+ * {Ftpd::FtpServer#nat_ip}
178
+ * {Ftpd::FtpServer#passive_ports}
179
179
  * {Ftpd::FtpServer#allow_low_data_ports}
180
180
  * {Ftpd::FtpServer#auth_level}
181
181
  * {Ftpd::FtpServer#exception_handler}
data/VERSION CHANGED
@@ -1 +1 @@
1
- 2.0.0
1
+ 2.0.1
data/bin/ftpdrb ADDED
@@ -0,0 +1,289 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ # Ftpd is a pure Ruby FTP server library. CLI version
4
+ #
5
+ unless $:.include?(File.dirname(__FILE__) + '/../lib')
6
+ $:.unshift(File.dirname(__FILE__) + '/../lib')
7
+ end
8
+
9
+ require 'ftpd'
10
+ require 'ipaddr'
11
+ require 'optparse'
12
+
13
+ module FTPDrb
14
+
15
+ # Command-line option parser
16
+ class Arguments
17
+
18
+ attr_reader :account
19
+ attr_reader :auth_level
20
+ attr_reader :debug
21
+ attr_reader :eplf
22
+ attr_reader :interface
23
+ attr_reader :nat_ip
24
+ attr_reader :passive_ports
25
+ attr_reader :password
26
+ attr_reader :port
27
+ attr_reader :path
28
+ attr_reader :read_only
29
+ attr_reader :session_timeout
30
+ attr_reader :servername
31
+ attr_reader :tls
32
+ attr_reader :user
33
+
34
+ def initialize(argv)
35
+ @interface = '127.0.0.1'
36
+ @tls = :explicit
37
+ @port = 0
38
+ @auth_level = 'password'
39
+ @user = ENV['LOGNAME']
40
+ @password = ''
41
+ @account = ''
42
+ @path = nil
43
+ @session_timeout = default_session_timeout
44
+ @servername = 'FTPd - Pure Ruby FTP server'
45
+ @log = nil
46
+ @nat_ip = nil
47
+ @passive_ports = nil
48
+
49
+ opts = option_parser
50
+ opts.parse!(argv)
51
+ rescue OptionParser::ParseError => e
52
+ $stderr.puts e
53
+ exit(1)
54
+ end
55
+
56
+ private
57
+
58
+ def option_parser
59
+ opts = OptionParser.new do |op|
60
+ op.banner = "ftpdrb - Pure Ruby FTP Server. (ftpd ruby gem)\n\nUsage: #{__FILE__} [OPTIONS]"
61
+
62
+ op.separator "Help menu:"
63
+ op.on('-p', '--port N', Integer, 'Bind to a specific port') do |t|
64
+ @port = t
65
+ end
66
+ op.on('-i', '--interface IP', 'Bind to a specific interface. (Use "0.0.0.0" for all interfaces)') do |t|
67
+ @interface = t
68
+ end
69
+ op.on('-s', '--servername SERVERNAME', 'Set servername for FTP server)',
70
+ "\tdefault = FTPd - Pure Ruby FTP server") do |t|
71
+ @servername = t
72
+ end
73
+ op.on('--tls [TYPE]', [:off, :explicit, :implicit],
74
+ 'Select TLS support (off, explicit, implicit)',
75
+ "\tdefault = off") do |t|
76
+ @tls = t
77
+ end
78
+ op.on('--eplf', 'LIST uses EPLF format') do |t|
79
+ @eplf = t
80
+ end
81
+ op.on('--read-only', 'Prohibit put, delete, rmdir, etc.') do |t|
82
+ @read_only = t
83
+ end
84
+ op.on('--auth [LEVEL]', [:user, :password, :account],
85
+ 'Set authorization level (user, password, account)',
86
+ "\tdefault = password") do |t|
87
+ @auth_level = t
88
+ end
89
+ op.on('-U', '--user NAME', 'User for authentication',
90
+ "\tdefaults to current user") do |t|
91
+ @user = t
92
+ end
93
+ op.on('-P', '--password PASS', 'Password for authentication',
94
+ "\tdefaults to empty string") do |t|
95
+ @password = t
96
+ end
97
+ op.on('-A', '--account P', 'Account for authentication',
98
+ "\tdefaults to empty string") do |t|
99
+ @account = t
100
+ end
101
+ op.on('--path PATH', 'Directory path for share',
102
+ "\tdefaults to random temp directory") do |t|
103
+ @path = t
104
+ end
105
+ op.on('--timeout SEC', Integer, 'Session idle timeout',
106
+ "\tdefaults to #{default_session_timeout}") do |t|
107
+ @session_timeout = t
108
+ end
109
+ op.on('-d', '--debug', 'Write server debug log to stdout') do |t|
110
+ @debug = t
111
+ end
112
+ op.on('--nat-ip IP', 'Set advertised passive mode IP') do |t|
113
+ @nat_ip = t
114
+ end
115
+ op.on('--ports MIN..MAX', 'Port numbers for passive mode sockets') do |v|
116
+ @passive_ports = Range.new(*v.split(/\.\./).map(&:to_i))
117
+ end
118
+ op.on("-h","--help","Show the full help screen.") do
119
+ puts opts
120
+ puts "\nExample:"
121
+ puts " #{__FILE__} -i 0.0.0.0 -p 21 -U ftp -P ftp --path /tmp/\n\n"
122
+ exit 0
123
+ end
124
+ end
125
+ end
126
+
127
+ def default_session_timeout
128
+ Ftpd::FtpServer::DEFAULT_SESSION_TIMEOUT
129
+ end
130
+
131
+ end
132
+ end
133
+
134
+ module FTPDrb
135
+
136
+ # The FTP server requires and instance of a _driver_ which can
137
+ # authenticate users and create a file system drivers for a given
138
+ # user.
139
+
140
+ class Driver
141
+
142
+ # Your driver's initialize method can be anything you need. Ftpd
143
+ # does not create an instance of your driver.
144
+
145
+ def initialize(user, password, account, data_dir, read_only)
146
+ @user = user
147
+ @password = password
148
+ @account = account
149
+ @data_dir = data_dir
150
+ @read_only = read_only
151
+ end
152
+
153
+ # Return true if the user should be allowed to log in.
154
+ # @param user [String]
155
+ # @param password [String]
156
+ # @param account [String]
157
+ # @return [Boolean]
158
+ #
159
+ # Depending upon the server's auth_level, some of these parameters
160
+ # may be nil. A parameter with a nil value is not required for
161
+ # authentication. Here are the parameters that are non-nil for
162
+ # each auth_level:
163
+ # * :user (user)
164
+ # * :password (user, password)
165
+ # * :account (user, password, account)
166
+
167
+ def authenticate(user, password, account)
168
+ user == @user &&
169
+ (password.nil? || password == @password) &&
170
+ (account.nil? || account == @account)
171
+ end
172
+
173
+ # Return the file system to use for a user.
174
+ # @param user [String]
175
+ # @return A file system driver that quacks like {Ftpd::DiskFileSystem}
176
+
177
+ def file_system(user)
178
+ if @read_only
179
+ Ftpd::ReadOnlyDiskFileSystem
180
+ else
181
+ Ftpd::DiskFileSystem.new(@data_dir)
182
+ end
183
+ end
184
+
185
+ end
186
+ end
187
+
188
+ module FTPDrb
189
+ class Server
190
+
191
+ include Ftpd::InsecureCertificate
192
+
193
+ def initialize(argv)
194
+ @args = Arguments.new(argv)
195
+ # Create a temp directory if path argument is not assigned
196
+ @args.path.nil? ? @data_dir = Ftpd::TempDir.make : @data_dir = @args.path
197
+ create_files if @args.path.nil?
198
+ @driver = Driver.new(user, password, account,
199
+ @data_dir, @args.read_only)
200
+ @server = Ftpd::FtpServer.new(@driver)
201
+ configure_server
202
+ @server.start
203
+ end
204
+
205
+ def run
206
+ puts "FTP server started. (Press CRL+C to stop it)"
207
+ connection_info.each do |key, value|
208
+ puts "#{key}: #{value}"
209
+ end
210
+ $stdout.flush
211
+ begin
212
+ loop{}
213
+ rescue Interrupt
214
+ puts "\nClosing FTP server..."
215
+ end
216
+ end
217
+
218
+ private
219
+ def configure_server
220
+ @server.server_name = @args.servername
221
+ @server.interface = @args.interface
222
+ @server.port = @args.port
223
+ @server.tls = @args.tls
224
+ @server.passive_ports = @args.passive_ports
225
+ @server.certfile_path = insecure_certfile_path
226
+ if @args.eplf
227
+ @server.list_formatter = Ftpd::ListFormat::Eplf
228
+ end
229
+ @server.auth_level = auth_level
230
+ @server.session_timeout = @args.session_timeout
231
+ @server.log = make_log
232
+ @server.nat_ip = @args.nat_ip
233
+ end
234
+
235
+ def auth_level
236
+ Ftpd.const_get("AUTH_#{@args.auth_level.upcase}")
237
+ end
238
+
239
+ def create_files
240
+ create_file 'README',
241
+ "Automatically created by FTPdrb.\n" +
242
+ "This file, and the directory it is in, will be deleted\n" +
243
+ "When ftpd server exits. Make sure your data is backed-up.\n"
244
+ end
245
+
246
+ def create_file(path, contents)
247
+ full_path = File.expand_path(path, @data_dir)
248
+ FileUtils.mkdir_p File.dirname(full_path)
249
+ File.open(full_path, 'w') do |file|
250
+ file.write contents
251
+ end
252
+ end
253
+
254
+ def connection_info
255
+ {
256
+ "Servername"=> @server.server_name,
257
+ "Interface" => @server.interface,
258
+ "Port" => @server.bound_port,
259
+ "User" => user,
260
+ "Pass" => "#{password if auth_level >= Ftpd::AUTH_PASSWORD}",
261
+ "Account" => "#{account if auth_level >= Ftpd::AUTH_ACCOUNT}",
262
+ "TLS" => @args.tls,
263
+ "Directory" => @data_dir,
264
+ "URI" => "ftp://#{user}:#{password}@#{IPAddr.new(@server.interface)}:#{@server.bound_port}",
265
+ "PID" => $$
266
+ }
267
+ end
268
+
269
+ def user
270
+ @args.user
271
+ end
272
+
273
+ def password
274
+ @args.password
275
+ end
276
+
277
+ def account
278
+ @args.account
279
+ end
280
+
281
+ def make_log
282
+ @args.debug && Logger.new($stdout)
283
+ end
284
+
285
+ end
286
+ end
287
+
288
+ FTPDrb::Server.new(ARGV).run if $0 == __FILE__
289
+
data/ftpd.gemspec CHANGED
@@ -2,18 +2,19 @@
2
2
  # DO NOT EDIT THIS FILE DIRECTLY
3
3
  # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
4
4
  # -*- encoding: utf-8 -*-
5
- # stub: ftpd 2.0.0 ruby lib
5
+ # stub: ftpd 2.0.1 ruby lib
6
6
 
7
7
  Gem::Specification.new do |s|
8
8
  s.name = "ftpd"
9
- s.version = "2.0.0"
9
+ s.version = "2.0.1"
10
10
 
11
11
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
12
12
  s.require_paths = ["lib"]
13
13
  s.authors = ["Wayne Conrad"]
14
- s.date = "2016-06-02"
14
+ s.date = "2016-11-02"
15
15
  s.description = "ftpd is a pure Ruby FTP server library. It supports implicit and explicit TLS, IPV6, passive and active mode, and is unconditionally compliant per RFC-1123. It can be used as part of a test fixture or embedded in a program."
16
16
  s.email = "wconrad@yagni.com"
17
+ s.executables = ["ftpdrb"]
17
18
  s.extra_rdoc_files = [
18
19
  "LICENSE.md",
19
20
  "README.md"
@@ -28,6 +29,7 @@ Gem::Specification.new do |s|
28
29
  "README.md",
29
30
  "Rakefile",
30
31
  "VERSION",
32
+ "bin/ftpdrb",
31
33
  "doc/benchmarks.md",
32
34
  "doc/references.md",
33
35
  "doc/rfc-compliance.md",
@@ -244,7 +246,7 @@ Gem::Specification.new do |s|
244
246
  ]
245
247
  s.homepage = "http://github.com/wconrad/ftpd"
246
248
  s.licenses = ["MIT"]
247
- s.rubygems_version = "2.4.8"
249
+ s.rubygems_version = "2.5.1"
248
250
  s.summary = "Pure Ruby FTP server library"
249
251
 
250
252
  if s.respond_to? :specification_version then
@@ -253,7 +255,7 @@ Gem::Specification.new do |s|
253
255
  if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
254
256
  s.add_runtime_dependency(%q<memoizer>, ["~> 1.0"])
255
257
  s.add_development_dependency(%q<cucumber>, ["~> 2.0"])
256
- s.add_development_dependency(%q<double-bag-ftps>, ["~> 0.1"])
258
+ s.add_development_dependency(%q<double-bag-ftps>, [">= 0.1.3", "~> 0.1"])
257
259
  s.add_development_dependency(%q<jeweler>, ["~> 2.0"])
258
260
  s.add_development_dependency(%q<rake>, ["~> 11.1"])
259
261
  s.add_development_dependency(%q<redcarpet>, ["~> 3.1"])
@@ -264,7 +266,7 @@ Gem::Specification.new do |s|
264
266
  else
265
267
  s.add_dependency(%q<memoizer>, ["~> 1.0"])
266
268
  s.add_dependency(%q<cucumber>, ["~> 2.0"])
267
- s.add_dependency(%q<double-bag-ftps>, ["~> 0.1"])
269
+ s.add_dependency(%q<double-bag-ftps>, [">= 0.1.3", "~> 0.1"])
268
270
  s.add_dependency(%q<jeweler>, ["~> 2.0"])
269
271
  s.add_dependency(%q<rake>, ["~> 11.1"])
270
272
  s.add_dependency(%q<redcarpet>, ["~> 3.1"])
@@ -276,7 +278,7 @@ Gem::Specification.new do |s|
276
278
  else
277
279
  s.add_dependency(%q<memoizer>, ["~> 1.0"])
278
280
  s.add_dependency(%q<cucumber>, ["~> 2.0"])
279
- s.add_dependency(%q<double-bag-ftps>, ["~> 0.1"])
281
+ s.add_dependency(%q<double-bag-ftps>, [">= 0.1.3", "~> 0.1"])
280
282
  s.add_dependency(%q<jeweler>, ["~> 2.0"])
281
283
  s.add_dependency(%q<rake>, ["~> 11.1"])
282
284
  s.add_dependency(%q<redcarpet>, ["~> 3.1"])
@@ -54,7 +54,7 @@ module Ftpd
54
54
  s = gets_with_timeout(socket)
55
55
  throw :done if s.nil?
56
56
  s = s.chomp
57
- config.log.debug s
57
+ config.log.debug s.sub(/^PASS .*/, 'PASS **FILTERED**') # Filter real password
58
58
  s
59
59
  end
60
60
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ftpd
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.0
4
+ version: 2.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Wayne Conrad
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-06-02 00:00:00.000000000 Z
11
+ date: 2016-11-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: memoizer
@@ -42,6 +42,9 @@ dependencies:
42
42
  name: double-bag-ftps
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: 0.1.3
45
48
  - - "~>"
46
49
  - !ruby/object:Gem::Version
47
50
  version: '0.1'
@@ -49,6 +52,9 @@ dependencies:
49
52
  prerelease: false
50
53
  version_requirements: !ruby/object:Gem::Requirement
51
54
  requirements:
55
+ - - ">="
56
+ - !ruby/object:Gem::Version
57
+ version: 0.1.3
52
58
  - - "~>"
53
59
  - !ruby/object:Gem::Version
54
60
  version: '0.1'
@@ -154,7 +160,8 @@ description: ftpd is a pure Ruby FTP server library. It supports implicit and e
154
160
  TLS, IPV6, passive and active mode, and is unconditionally compliant per RFC-1123. It
155
161
  can be used as part of a test fixture or embedded in a program.
156
162
  email: wconrad@yagni.com
157
- executables: []
163
+ executables:
164
+ - ftpdrb
158
165
  extensions: []
159
166
  extra_rdoc_files:
160
167
  - LICENSE.md
@@ -169,6 +176,7 @@ files:
169
176
  - README.md
170
177
  - Rakefile
171
178
  - VERSION
179
+ - bin/ftpdrb
172
180
  - doc/benchmarks.md
173
181
  - doc/references.md
174
182
  - doc/rfc-compliance.md
@@ -402,7 +410,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
402
410
  version: '0'
403
411
  requirements: []
404
412
  rubyforge_project:
405
- rubygems_version: 2.4.8
413
+ rubygems_version: 2.5.1
406
414
  signing_key:
407
415
  specification_version: 4
408
416
  summary: Pure Ruby FTP server library