em-ftpd 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/Gemfile +3 -0
- data/MIT-LICENSE +20 -0
- data/README.markdown +149 -0
- data/bin/em-ftpd +18 -0
- data/examples/fake.rb +90 -0
- data/examples/redis.rb +121 -0
- data/lib/em-ftpd.rb +12 -0
- data/lib/em-ftpd/active_socket.rb +14 -0
- data/lib/em-ftpd/app.rb +88 -0
- data/lib/em-ftpd/authentication.rb +48 -0
- data/lib/em-ftpd/base_socket.rb +47 -0
- data/lib/em-ftpd/configurator.rb +120 -0
- data/lib/em-ftpd/directories.rb +110 -0
- data/lib/em-ftpd/directory_item.rb +12 -0
- data/lib/em-ftpd/files.rb +141 -0
- data/lib/em-ftpd/passive_socket.rb +30 -0
- data/lib/em-ftpd/server.rb +343 -0
- metadata +158 -0
data/Gemfile
ADDED
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2008 James Healy
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.markdown
ADDED
@@ -0,0 +1,149 @@
|
|
1
|
+
# em-ftpd
|
2
|
+
|
3
|
+
A mini-FTP server framework built on top of the EventMacine gem. By providing a
|
4
|
+
simple driver class that responds to a handful of methods you can have a
|
5
|
+
complete FTP server.
|
6
|
+
|
7
|
+
The library is extracted from real world situations where an FTP interface was
|
8
|
+
required to sit in front of a non-filesystem persistence layer.
|
9
|
+
|
10
|
+
Some sample use cases include persisting data to:
|
11
|
+
|
12
|
+
* an Amazon S3 bucket
|
13
|
+
* a relational database
|
14
|
+
* redis
|
15
|
+
* memory
|
16
|
+
|
17
|
+
For some examples that demonstrate redis and memory persistence, check the
|
18
|
+
examples/ directory.
|
19
|
+
|
20
|
+
## Usage
|
21
|
+
|
22
|
+
To boot an FTP server you will need to provide a driver that speaks to your
|
23
|
+
persistence layer.
|
24
|
+
|
25
|
+
Create a config.rb file that loads the driver and then configures the server
|
26
|
+
|
27
|
+
require 'my_fancy_driver'
|
28
|
+
|
29
|
+
driver MyFancyDriver
|
30
|
+
user 'ftp'
|
31
|
+
group 'ftp'
|
32
|
+
|
33
|
+
Run your server like so:
|
34
|
+
|
35
|
+
em-ftpd config.rb
|
36
|
+
|
37
|
+
## Config File
|
38
|
+
|
39
|
+
Valid options for the config file are:
|
40
|
+
|
41
|
+
* user [name of system user to run the process as]
|
42
|
+
* group [name of group to run the process as]
|
43
|
+
* daemonise [true/false]
|
44
|
+
* name [a string to include in the process description]
|
45
|
+
* pid_file [a path to save the pid to. Useful in conjunction with daemonise]
|
46
|
+
* port [the TCP port to bind to. Defaults to 21]
|
47
|
+
* driver [the class that connects to the persistance layer]
|
48
|
+
* driver_args [any arguments that need to be passed to the driver constructor]
|
49
|
+
|
50
|
+
## The Driver Contract
|
51
|
+
|
52
|
+
The driver MUST have the following methods. Each method MUST accept a block and
|
53
|
+
yield the appropriate value:
|
54
|
+
|
55
|
+
authenticate(user, pass, &block)
|
56
|
+
- boolean indicating if the provided details are valid
|
57
|
+
|
58
|
+
bytes(path, &block)
|
59
|
+
- an integer with the number of bytes in the file or nil if the file
|
60
|
+
doesn't exist
|
61
|
+
|
62
|
+
change_dir(path, &block)
|
63
|
+
- a boolen indicating if the current user is permitted to change to the
|
64
|
+
requested path
|
65
|
+
|
66
|
+
dir_contents(path, &block)
|
67
|
+
- an array of the contents of the requested path or nil if the dir
|
68
|
+
doesn't exist. Each entry in the array should be
|
69
|
+
EM::FTPD::DirectoryItem-ish
|
70
|
+
|
71
|
+
delete_dir(path, &block)
|
72
|
+
- a boolean indicating if the directory was successfully deleted
|
73
|
+
|
74
|
+
delete_file(path, &block)
|
75
|
+
- a boolean indicating if path was successfully deleted
|
76
|
+
|
77
|
+
rename(from_path, to_path, &block)
|
78
|
+
- a boolean indicating if from_path was successfully renamed to to_path
|
79
|
+
|
80
|
+
make_dir(path, &block)
|
81
|
+
- a boolean indicating if path was successfully created as a new directory
|
82
|
+
|
83
|
+
get_file(path, &block)
|
84
|
+
- nil if the user isn't permitted to access that path
|
85
|
+
- an IOish (File, StringIO, IO, etc) object with data to send back to the
|
86
|
+
client
|
87
|
+
- a string with the file data to send to the client
|
88
|
+
- an array of strings to join with the standard FTP line break and send to
|
89
|
+
the client
|
90
|
+
|
91
|
+
The driver MUST have one of the following methods. Each method MUST accept a
|
92
|
+
block and yield the appropriate value:
|
93
|
+
|
94
|
+
put_file(path, tmp_file_path, &block)
|
95
|
+
- an integer indicating the number of bytes received or False if there
|
96
|
+
was an error
|
97
|
+
|
98
|
+
put_file_streamed(path, datasocket, &block)
|
99
|
+
- an integer indicating the number of bytes received or False if there
|
100
|
+
was an error
|
101
|
+
|
102
|
+
## Authors
|
103
|
+
|
104
|
+
James Healy <james@yob.id.au> [http://www.yob.id.au](http://www.yob.id.au)
|
105
|
+
John Nunemaker <nunemaker@gmail.com>
|
106
|
+
Elijah Miller <elijah.miller@gmail.com>
|
107
|
+
|
108
|
+
## Warning
|
109
|
+
|
110
|
+
FTP is an incredibly insecure protocol. Be careful about forcing users to authenticate
|
111
|
+
with a username or password that are important.
|
112
|
+
|
113
|
+
## License
|
114
|
+
|
115
|
+
This library is distributed under the terms of the MIT License. See the included file for
|
116
|
+
more detail.
|
117
|
+
|
118
|
+
## Contributing
|
119
|
+
|
120
|
+
All suggestions and patches welcome, preferably via a git repository I can pull from.
|
121
|
+
If this library proves useful to you, please let me know.
|
122
|
+
|
123
|
+
## Further Reading
|
124
|
+
|
125
|
+
There are a range of RFCs that together specify the FTP protocol. In chronological
|
126
|
+
order, the more useful ones are:
|
127
|
+
|
128
|
+
- [http://tools.ietf.org/rfc/rfc959.txt](http://tools.ietf.org/rfc/rfc959.txt)
|
129
|
+
- [http://tools.ietf.org/rfc/rfc1123.txt](http://tools.ietf.org/rfc/rfc1123.txt)
|
130
|
+
- [http://tools.ietf.org/rfc/rfc2228.txt](http://tools.ietf.org/rfc/rfc2228.txt)
|
131
|
+
- [http://tools.ietf.org/rfc/rfc2389.txt](http://tools.ietf.org/rfc/rfc2389.txt)
|
132
|
+
- [http://tools.ietf.org/rfc/rfc2428.txt](http://tools.ietf.org/rfc/rfc2428.txt)
|
133
|
+
- [http://tools.ietf.org/rfc/rfc3659.txt](http://tools.ietf.org/rfc/rfc3659.txt)
|
134
|
+
- [http://tools.ietf.org/rfc/rfc4217.txt](http://tools.ietf.org/rfc/rfc4217.txt)
|
135
|
+
|
136
|
+
For an english summary that's somewhat more legible than the RFCs, and provides
|
137
|
+
some commentary on what features are actually useful or relevant 24 years after
|
138
|
+
RFC959 was published:
|
139
|
+
|
140
|
+
- [http://cr.yp.to/ftp.html](http://cr.yp.to/ftp.html)
|
141
|
+
|
142
|
+
For a history lesson, check out Appendix III of RCF959. It lists the preceding
|
143
|
+
(obsolete) RFC documents that relate to file transfers, including the ye old
|
144
|
+
RFC114 from 1971, "A File Transfer Protocol"
|
145
|
+
|
146
|
+
For more information on EventMacine, a library that (among other things) simplifies
|
147
|
+
writing applications that use sockets, check out their website.
|
148
|
+
|
149
|
+
- [http://rubyeventmachine.com/](http://rubyeventmachine.com/)
|
data/bin/em-ftpd
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
#!/usr/bin/ruby
|
2
|
+
|
3
|
+
# Boot an FTP server
|
4
|
+
#
|
5
|
+
# Usage:
|
6
|
+
#
|
7
|
+
# em-ftpd config.rb
|
8
|
+
|
9
|
+
require 'em-ftpd'
|
10
|
+
|
11
|
+
config_file = ARGV.first
|
12
|
+
|
13
|
+
if config_file && File.file?(config_file)
|
14
|
+
EM::FTPD::App.start(config_file)
|
15
|
+
else
|
16
|
+
$stderr.puts "USAGE: em-ftpd <config.rb>"
|
17
|
+
exit 1
|
18
|
+
end
|
data/examples/fake.rb
ADDED
@@ -0,0 +1,90 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
# a super simple FTP server with hard coded auth details and only two files
|
4
|
+
# available for download.
|
5
|
+
#
|
6
|
+
# Usage:
|
7
|
+
#
|
8
|
+
# em-ftpd examples/fake.rb
|
9
|
+
|
10
|
+
class FakeFTPDriver
|
11
|
+
FILE_ONE = "This is the first file available for download.\n\nBy James"
|
12
|
+
FILE_TWO = "This is the file number two.\n\n2009-03-21"
|
13
|
+
|
14
|
+
def change_dir(path, &block)
|
15
|
+
yield path == "/" || path == "/files"
|
16
|
+
end
|
17
|
+
|
18
|
+
def dir_contents(path, &block)
|
19
|
+
case path
|
20
|
+
when "/" then
|
21
|
+
yield [ dir_item("files"), file_item("one.txt", FILE_ONE.bytesize) ]
|
22
|
+
when "/files" then
|
23
|
+
yield [ file_item("two.txt", FILE_TWO.bytesize) ]
|
24
|
+
else
|
25
|
+
yield []
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def authenticate(user, pass, &block)
|
30
|
+
yield user == "test" && pass == "1234"
|
31
|
+
end
|
32
|
+
|
33
|
+
def bytes(path, &block)
|
34
|
+
yield case path
|
35
|
+
when "/one.txt" then FILE_ONE.size
|
36
|
+
when "/files/two.txt" then FILE_TWO.size
|
37
|
+
else
|
38
|
+
false
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def get_file(path, &block)
|
43
|
+
yield case path
|
44
|
+
when "/one.txt" then FILE_ONE
|
45
|
+
when "/files/two.txt" then FILE_TWO
|
46
|
+
else
|
47
|
+
false
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def put_file(path, data, &block)
|
52
|
+
yield false
|
53
|
+
end
|
54
|
+
|
55
|
+
def delete_file(path, &block)
|
56
|
+
yield false
|
57
|
+
end
|
58
|
+
|
59
|
+
def delete_dir(path, &block)
|
60
|
+
yield false
|
61
|
+
end
|
62
|
+
|
63
|
+
def rename(from, to, &block)
|
64
|
+
yield false
|
65
|
+
end
|
66
|
+
|
67
|
+
def make_dir(path, &block)
|
68
|
+
yield false
|
69
|
+
end
|
70
|
+
|
71
|
+
private
|
72
|
+
|
73
|
+
def dir_item(name)
|
74
|
+
EM::FTPD::DirectoryItem.new(:name => name, :directory => true, :size => 0)
|
75
|
+
end
|
76
|
+
|
77
|
+
def file_item(name, bytes)
|
78
|
+
EM::FTPD::DirectoryItem.new(:name => name, :directory => false, :size => bytes)
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
82
|
+
|
83
|
+
# configure the server
|
84
|
+
driver FakeFTPDriver
|
85
|
+
#driver_args 1, 2, 3
|
86
|
+
#user "ftp"
|
87
|
+
#group "ftp"
|
88
|
+
#daemonise false
|
89
|
+
#name "fakeftp"
|
90
|
+
#pid_file "/var/run/fakeftp.pid"
|
data/examples/redis.rb
ADDED
@@ -0,0 +1,121 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
# an FTP server that uses redis for persistance.
|
4
|
+
#
|
5
|
+
# NOTE: This may not be working as I don't have redis installed
|
6
|
+
# to test it. Feel free to fix it and submit a patch
|
7
|
+
#
|
8
|
+
# Usage:
|
9
|
+
#
|
10
|
+
# em-ftpd examples/redis.rb
|
11
|
+
|
12
|
+
class RedisFTPDriver
|
13
|
+
|
14
|
+
def initialize(redis)
|
15
|
+
@redis = redis
|
16
|
+
end
|
17
|
+
|
18
|
+
def change_dir(path, &block)
|
19
|
+
yield path == "/" || @redis.sismember(directory_key(File.dirname(path)), File.basename(path) + "/")
|
20
|
+
end
|
21
|
+
|
22
|
+
def dir_contents(path, &block)
|
23
|
+
response = @redis.smembers(directory_key(path))
|
24
|
+
|
25
|
+
yield response.map do |key|
|
26
|
+
name, size = key.sub(/ftp:\//, '').sub(%r{/$}, '')
|
27
|
+
dir = key.match(%r{/$})
|
28
|
+
EM::FPD::DirectoryItem.new(
|
29
|
+
:name => name,
|
30
|
+
:directory => dir,
|
31
|
+
:size => size
|
32
|
+
)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def authenticate(user, pass, &block)
|
37
|
+
yield true
|
38
|
+
end
|
39
|
+
|
40
|
+
def get_file(path, &block)
|
41
|
+
yield @redis.get(file_data_key(path))
|
42
|
+
end
|
43
|
+
|
44
|
+
def put_file(path, data, &block)
|
45
|
+
@redis.set(file_data_key(path), data)
|
46
|
+
@redis.sadd(directory_key(File.dirname(path)), File.basename(path))
|
47
|
+
yield
|
48
|
+
end
|
49
|
+
|
50
|
+
def delete_file(path, &block)
|
51
|
+
@redis.del(file_data_key(path))
|
52
|
+
@redis.srem(directory_key(File.dirname(path)), File.basename(path))
|
53
|
+
yield true
|
54
|
+
end
|
55
|
+
|
56
|
+
|
57
|
+
def delete_dir(path, &block)
|
58
|
+
(@redis.keys(directory_key(path + "/*") + @redis.keys(file_data_key(path + "/*")))).each do |key|
|
59
|
+
@redis.del(key)
|
60
|
+
end
|
61
|
+
@redis.srem(directory_key(File.dirname(path), File.basename(path) + "/"))
|
62
|
+
yield true
|
63
|
+
end
|
64
|
+
|
65
|
+
def rename(from, to, &block)
|
66
|
+
if @redis.sismember(directory_key(File.dirname(from)), File.basename(from))
|
67
|
+
yield move_file(from, to)
|
68
|
+
elsif @redis.sismember(directory_key(File.dirname(from)), File.basename(from) + '/')
|
69
|
+
yield move_dir(from, to)
|
70
|
+
else
|
71
|
+
yield false
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def make_dir(path, &block)
|
76
|
+
@redis.sadd(directory_key(File.dirname(path)), File.basename(path) + "/")
|
77
|
+
yield true
|
78
|
+
end
|
79
|
+
|
80
|
+
private
|
81
|
+
|
82
|
+
def file_data_key(path)
|
83
|
+
"ftp:data:#{path}"
|
84
|
+
end
|
85
|
+
|
86
|
+
def directory_key(path)
|
87
|
+
"ftp:dir:#{path}"
|
88
|
+
end
|
89
|
+
|
90
|
+
def move_file(from, to)
|
91
|
+
@redis.rename(file_data_key(from), file_data_key(to))
|
92
|
+
@redis.srem(directory_key(File.dirname(from)), File.basename(from))
|
93
|
+
@redis.sadd(directory_key(File.dirname(to)), File.basename(to))
|
94
|
+
end
|
95
|
+
|
96
|
+
def move_dir(from, to)
|
97
|
+
if @redis.exists(directory_key(from))
|
98
|
+
@redis.rename(directory_key(from), directory_key(to))
|
99
|
+
end
|
100
|
+
@redis.srem(directory_key(File.dirname(from)), File.basename(from) + "/")
|
101
|
+
@redis.sadd(directory_key(File.dirname(to)), File.basename(to) + "/")
|
102
|
+
@redis.keys(directory_key(from + "/*")).each do |key|
|
103
|
+
new_key = directory_key(File.dirname(to)) + key.sub(directory_key(File.dirname(from)), '')
|
104
|
+
@redis.rename(key, new_key)
|
105
|
+
end
|
106
|
+
@redis.keys(file_data_key(from + "/*")).each do |key|
|
107
|
+
new_key = file_data_key(to) + key.sub(file_data_key(from), '/')
|
108
|
+
@redis.rename(key, new_key)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
end
|
113
|
+
|
114
|
+
# configure the server
|
115
|
+
driver FakeFTPDriver
|
116
|
+
#driver_args 1, 2, 3
|
117
|
+
#user "ftp"
|
118
|
+
#group "ftp"
|
119
|
+
#daemonise false
|
120
|
+
#name "fakeftp"
|
121
|
+
#pid_file "/var/run/fakeftp.pid"
|
data/lib/em-ftpd.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
require 'eventmachine'
|
2
|
+
|
3
|
+
require 'em-ftpd/authentication'
|
4
|
+
require 'em-ftpd/directories'
|
5
|
+
require 'em-ftpd/files'
|
6
|
+
require 'em-ftpd/directory_item'
|
7
|
+
require 'em-ftpd/server'
|
8
|
+
require 'em-ftpd/base_socket'
|
9
|
+
require 'em-ftpd/passive_socket'
|
10
|
+
require 'em-ftpd/active_socket'
|
11
|
+
require 'em-ftpd/configurator'
|
12
|
+
require 'em-ftpd/app'
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module EM::FTPD
|
2
|
+
# An eventmachine module for connecting to a remote
|
3
|
+
# port and downloading a file
|
4
|
+
#
|
5
|
+
class ActiveSocket < EventMachine::Connection
|
6
|
+
include EM::Deferrable
|
7
|
+
include BaseSocket
|
8
|
+
|
9
|
+
def self.open(host, port)
|
10
|
+
EventMachine.connect(host, port, self)
|
11
|
+
end
|
12
|
+
|
13
|
+
end
|
14
|
+
end
|
data/lib/em-ftpd/app.rb
ADDED
@@ -0,0 +1,88 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
require 'singleton'
|
4
|
+
|
5
|
+
module EM::FTPD
|
6
|
+
|
7
|
+
class App
|
8
|
+
include Singleton
|
9
|
+
|
10
|
+
def daemonise!(config)
|
11
|
+
return unless config.daemonise
|
12
|
+
|
13
|
+
## close unneeded descriptors,
|
14
|
+
$stdin.reopen("/dev/null")
|
15
|
+
$stdout.reopen("/dev/null","w")
|
16
|
+
$stderr.reopen("/dev/null","w")
|
17
|
+
|
18
|
+
## drop into the background.
|
19
|
+
pid = fork
|
20
|
+
if pid
|
21
|
+
## parent: save pid of child, then exit
|
22
|
+
if config.pid_file
|
23
|
+
File.open(config.pid_file, "w") { |io| io.write pid }
|
24
|
+
end
|
25
|
+
exit!
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.start(config_path)
|
30
|
+
self.instance.start(config_path)
|
31
|
+
end
|
32
|
+
|
33
|
+
def start(config_path)
|
34
|
+
config_data = File.read(config_path)
|
35
|
+
config = EM::FTPD::Configurator.new
|
36
|
+
config.instance_eval(config_data)
|
37
|
+
config.check!
|
38
|
+
update_procline(config.name)
|
39
|
+
|
40
|
+
EventMachine.epoll
|
41
|
+
|
42
|
+
EventMachine::run do
|
43
|
+
puts "Starting ftp server on 0.0.0.0:#{config.port}"
|
44
|
+
EventMachine::start_server("0.0.0.0", config.port, EM::FTPD::Server, config.driver, *config.driver_args)
|
45
|
+
|
46
|
+
daemonise!(config)
|
47
|
+
change_gid(config.gid)
|
48
|
+
change_uid(config.uid)
|
49
|
+
setup_signal_handlers
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
def update_procline(name)
|
56
|
+
if name
|
57
|
+
$0 = "em-ftp [#{name}]"
|
58
|
+
else
|
59
|
+
$0 = "em-ftp"
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def change_gid(gid)
|
64
|
+
if gid && Process.gid == 0
|
65
|
+
Process.gid = gid
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def change_uid(uid)
|
70
|
+
if uid && Process.euid == 0
|
71
|
+
Process::Sys.setuid(uid)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def setup_signal_handlers
|
76
|
+
trap('QUIT') do
|
77
|
+
EM.stop
|
78
|
+
end
|
79
|
+
trap('TERM') do
|
80
|
+
EM.stop
|
81
|
+
end
|
82
|
+
trap('INT') do
|
83
|
+
EM.stop
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
end
|
88
|
+
end
|