em-ftpd 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|