em-ftpd 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source :gemcutter
2
+
3
+ gemspec
@@ -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.
@@ -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/)
@@ -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
@@ -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"
@@ -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"
@@ -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
@@ -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