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 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