raval 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 +202 -0
- data/examples/fake.rb +110 -0
- data/examples/fog.rb +183 -0
- data/lib/raval.rb +8 -0
- data/lib/raval/active_socket.rb +49 -0
- data/lib/raval/app.rb +137 -0
- data/lib/raval/connection.rb +50 -0
- data/lib/raval/handler.rb +549 -0
- data/lib/raval/list_formatter.rb +48 -0
- data/lib/raval/passive_socket.rb +86 -0
- data/lib/raval/server.rb +45 -0
- data/spec/handler_spec.rb +832 -0
- data/spec/list_formatter_spec.rb +71 -0
- data/spec/spec_helper.rb +16 -0
- data/spec/support/test_driver.rb +88 -0
- metadata +165 -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,202 @@
|
|
1
|
+
# raval
|
2
|
+
|
3
|
+
An experimental FTP server framework built on top of Celluloid. Celluloid is a
|
4
|
+
concurrency library that uses threads, so you will get the best results by
|
5
|
+
using jruby or rubinius.
|
6
|
+
|
7
|
+
By providing a simple driver class that responds to a handful of methods you
|
8
|
+
can have a complete FTP server.
|
9
|
+
|
10
|
+
The library is extracted from real world situations where an FTP interface was
|
11
|
+
required to sit in front of a non-filesystem persistence layer.
|
12
|
+
|
13
|
+
Some sample use cases include persisting data to:
|
14
|
+
|
15
|
+
* an Amazon S3 bucket
|
16
|
+
* a relational database
|
17
|
+
* redis
|
18
|
+
* memory
|
19
|
+
|
20
|
+
The examples directory contains a demonstration of in memory persistence.
|
21
|
+
|
22
|
+
## Installation
|
23
|
+
|
24
|
+
gem install raval
|
25
|
+
|
26
|
+
## Usage
|
27
|
+
|
28
|
+
To boot an FTP server you will need to provide a driver that speaks to your
|
29
|
+
persistence layer - the required driver contract is listed below.
|
30
|
+
|
31
|
+
Once that's ready, boot a new server like so:
|
32
|
+
|
33
|
+
require 'raval'
|
34
|
+
|
35
|
+
class MyDriver
|
36
|
+
.. implementation here ..
|
37
|
+
end
|
38
|
+
|
39
|
+
Raval::App.start(:host => "127.0.0.1",
|
40
|
+
:port => 3000,
|
41
|
+
:driver => MyDriver)
|
42
|
+
|
43
|
+
To get started quickly, try out the example in the git repo like so:
|
44
|
+
|
45
|
+
ruby -Ilib examples/fake.rb
|
46
|
+
|
47
|
+
### The Driver Contract
|
48
|
+
|
49
|
+
The driver MUST have the following methods. Each method MUST accept the listed
|
50
|
+
parameters and return an appropriate value:
|
51
|
+
|
52
|
+
authenticate(user, pass)
|
53
|
+
- boolean indicating if the provided details are valid
|
54
|
+
|
55
|
+
bytes(path)
|
56
|
+
- an integer with the number of bytes in the file or nil if the file
|
57
|
+
doesn't exist
|
58
|
+
|
59
|
+
modified_time(path)
|
60
|
+
- a Time-ish object indicating when the requested path was last modified, or
|
61
|
+
nil if the file doesn't exist
|
62
|
+
|
63
|
+
change_dir(path)
|
64
|
+
- a boolean indicating if the current user is permitted to change to the
|
65
|
+
requested path
|
66
|
+
|
67
|
+
dir_contents(path)
|
68
|
+
- an array of the contents of the requested path or nil if the dir
|
69
|
+
doesn't exist. Each entry in the array should meet the Entry contract
|
70
|
+
defined below.
|
71
|
+
|
72
|
+
delete_dir(path)
|
73
|
+
- a boolean indicating if the directory was successfully deleted
|
74
|
+
|
75
|
+
delete_file(path)
|
76
|
+
- a boolean indicating if path was successfully deleted
|
77
|
+
|
78
|
+
rename(from_path, to_path)
|
79
|
+
- a boolean indicating if from_path was successfully renamed to to_path
|
80
|
+
|
81
|
+
make_dir(path)
|
82
|
+
- a boolean indicating if path was successfully created as a new directory
|
83
|
+
|
84
|
+
get_file(path)
|
85
|
+
- nil if the user isn't permitted to access that path
|
86
|
+
- an IOish (File, StringIO, IO, etc) object with data to send back to the
|
87
|
+
client
|
88
|
+
|
89
|
+
The driver MUST have one of the following methods. Each method MUST accept the
|
90
|
+
listed parameters and return an appropriate value:
|
91
|
+
|
92
|
+
put_file(path, tmp_file_path)
|
93
|
+
- an integer indicating the number of bytes received or False if there
|
94
|
+
was an error
|
95
|
+
|
96
|
+
put_file_streamed(path, datasocket)
|
97
|
+
- an integer indicating the number of bytes received or False if there
|
98
|
+
was an error
|
99
|
+
|
100
|
+
### The File Entry Contract
|
101
|
+
|
102
|
+
The Driver#dir_contents method must return an array of Entry objects that
|
103
|
+
describe each item in a directory. Each of these Entry objects MUST have
|
104
|
+
the following methods and return values. All methods (except name) can
|
105
|
+
return nil if you're happy for raval to use a reasonable default.
|
106
|
+
|
107
|
+
directory
|
108
|
+
- a boolean indicating if the entry is a directory
|
109
|
+
|
110
|
+
size
|
111
|
+
- the number of bytes in a file. 0 if the entry is a directory
|
112
|
+
|
113
|
+
permissions
|
114
|
+
- a string describing the current users permissions on the entry
|
115
|
+
|
116
|
+
owner
|
117
|
+
- the name of the user that owns the entry
|
118
|
+
|
119
|
+
group
|
120
|
+
- the name of the group that owns the entry
|
121
|
+
|
122
|
+
time
|
123
|
+
- a Time-ish object that indicates when the entry was last modified
|
124
|
+
|
125
|
+
name
|
126
|
+
- the name of the file or directory
|
127
|
+
|
128
|
+
### Driver Options
|
129
|
+
|
130
|
+
Some drivers may need configuration options - for server addresses or usernames
|
131
|
+
or whatever. To do so, just write your driver constructor to accept an options
|
132
|
+
hash, and then use it like so:
|
133
|
+
|
134
|
+
require 'raval'
|
135
|
+
|
136
|
+
class MyDriver
|
137
|
+
def initialize(opts = {})
|
138
|
+
|
139
|
+
end
|
140
|
+
|
141
|
+
.. remaining implementation here ..
|
142
|
+
end
|
143
|
+
|
144
|
+
Raval::App.start(:host => "127.0.0.1",
|
145
|
+
:port => 3000,
|
146
|
+
:driver => MyDriver,
|
147
|
+
:driver_opts => {:foo => :bar})
|
148
|
+
|
149
|
+
You can see an example of driver options in examples/fog.rb.
|
150
|
+
|
151
|
+
## Contributors
|
152
|
+
|
153
|
+
* James Healy <james@yob.id.au> [http://www.yob.id.au](http://www.yob.id.au)
|
154
|
+
* John Nunemaker <nunemaker@gmail.com>
|
155
|
+
* Elijah Miller <elijah.miller@gmail.com>
|
156
|
+
|
157
|
+
## Warning
|
158
|
+
|
159
|
+
FTP is an incredibly insecure protocol. Be careful about forcing users to authenticate
|
160
|
+
with a username or password that are important.
|
161
|
+
|
162
|
+
## License
|
163
|
+
|
164
|
+
This library is distributed under the terms of the MIT License. See the included file for
|
165
|
+
more detail.
|
166
|
+
|
167
|
+
## Contributing
|
168
|
+
|
169
|
+
All suggestions and patches welcome, preferably via a git repository I can pull from.
|
170
|
+
If this library proves useful to you, please let me know.
|
171
|
+
|
172
|
+
## Further Reading
|
173
|
+
|
174
|
+
There are a range of RFCs that together specify the FTP protocol. In chronological
|
175
|
+
order, the more useful ones are:
|
176
|
+
|
177
|
+
* [http://tools.ietf.org/rfc/rfc959.txt](http://tools.ietf.org/rfc/rfc959.txt)
|
178
|
+
* [http://tools.ietf.org/rfc/rfc1123.txt](http://tools.ietf.org/rfc/rfc1123.txt)
|
179
|
+
* [http://tools.ietf.org/rfc/rfc2228.txt](http://tools.ietf.org/rfc/rfc2228.txt)
|
180
|
+
* [http://tools.ietf.org/rfc/rfc2389.txt](http://tools.ietf.org/rfc/rfc2389.txt)
|
181
|
+
* [http://tools.ietf.org/rfc/rfc2428.txt](http://tools.ietf.org/rfc/rfc2428.txt)
|
182
|
+
* [http://tools.ietf.org/rfc/rfc3659.txt](http://tools.ietf.org/rfc/rfc3659.txt)
|
183
|
+
* [http://tools.ietf.org/rfc/rfc4217.txt](http://tools.ietf.org/rfc/rfc4217.txt)
|
184
|
+
|
185
|
+
For an english summary that's somewhat more legible than the RFCs, and provides
|
186
|
+
some commentary on what features are actually useful or relevant 24 years after
|
187
|
+
RFC959 was published:
|
188
|
+
|
189
|
+
* [http://cr.yp.to/ftp.html](http://cr.yp.to/ftp.html)
|
190
|
+
|
191
|
+
For a history lesson, check out Appendix III of RCF959. It lists the preceding
|
192
|
+
(obsolete) RFC documents that relate to file transfers, including the ye old
|
193
|
+
RFC114 from 1971, "A File Transfer Protocol"
|
194
|
+
|
195
|
+
For more information on Celluloid, a library that (among other things) simplifies
|
196
|
+
writing applications that use sockets, check out their website.
|
197
|
+
|
198
|
+
* [http://celluloid.io/](http://celluloid.io/)
|
199
|
+
|
200
|
+
This library is heavily based on [em-ftpd](https://github.com/yob/em-ftpd), an FTPd
|
201
|
+
framework with similar design goals within the EventMachine ecosystem. It worked well
|
202
|
+
enough, but you know, callbacks and event loops make me something something.
|
data/examples/fake.rb
ADDED
@@ -0,0 +1,110 @@
|
|
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
|
+
# The try it out, run this:
|
7
|
+
#
|
8
|
+
# ruby -Ilib examples/fake.rb
|
9
|
+
|
10
|
+
require 'ostruct'
|
11
|
+
require 'stringio'
|
12
|
+
require 'raval'
|
13
|
+
|
14
|
+
class FakeFTPDriver
|
15
|
+
FILE_ONE = "This is the first file available for download.\n\nBy James"
|
16
|
+
FILE_TWO = "This is the file number two.\n\n2009-03-21"
|
17
|
+
|
18
|
+
def change_dir(path)
|
19
|
+
path == "/" || path == "/files"
|
20
|
+
end
|
21
|
+
|
22
|
+
def dir_contents(path)
|
23
|
+
case path
|
24
|
+
when "/" then
|
25
|
+
[ dir_item("files"), file_item("one.txt", FILE_ONE.bytesize) ]
|
26
|
+
when "/files" then
|
27
|
+
[ file_item("two.txt", FILE_TWO.bytesize) ]
|
28
|
+
else
|
29
|
+
[]
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def authenticate(user, pass)
|
34
|
+
user == "test" && pass == "1234"
|
35
|
+
end
|
36
|
+
|
37
|
+
def bytes(path)
|
38
|
+
case path
|
39
|
+
when "/one.txt" then FILE_ONE.bytesize
|
40
|
+
when "/files/two.txt" then FILE_TWO.bytesize
|
41
|
+
else
|
42
|
+
false
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def modified_time(path)
|
47
|
+
case path
|
48
|
+
when "/one.txt" then Time.now
|
49
|
+
when "/files/two.txt" then Time.now - 10000
|
50
|
+
else
|
51
|
+
false
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def get_file(path)
|
56
|
+
case path
|
57
|
+
when "/one.txt" then StringIO.new(FILE_ONE)
|
58
|
+
when "/files/two.txt" then StringIO.new(FILE_TWO)
|
59
|
+
else
|
60
|
+
false
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def put_file(path, tmp_path)
|
65
|
+
File.size(tmp_path)
|
66
|
+
end
|
67
|
+
|
68
|
+
def delete_file(path)
|
69
|
+
false
|
70
|
+
end
|
71
|
+
|
72
|
+
def delete_dir(path)
|
73
|
+
false
|
74
|
+
end
|
75
|
+
|
76
|
+
def rename(from, to)
|
77
|
+
false
|
78
|
+
end
|
79
|
+
|
80
|
+
def make_dir(path)
|
81
|
+
false
|
82
|
+
end
|
83
|
+
|
84
|
+
private
|
85
|
+
|
86
|
+
def dir_item(name)
|
87
|
+
OpenStruct.new(:name => name,
|
88
|
+
:directory => true,
|
89
|
+
:size => 0,
|
90
|
+
:owner => "user",
|
91
|
+
:group => "group",
|
92
|
+
:time => Time.now,
|
93
|
+
:permissions => "r--r--r--")
|
94
|
+
end
|
95
|
+
|
96
|
+
def file_item(name, bytes)
|
97
|
+
OpenStruct.new(:name => name,
|
98
|
+
:directory => false,
|
99
|
+
:size => bytes,
|
100
|
+
:owner => "user",
|
101
|
+
:group => "group",
|
102
|
+
:time => Time.now,
|
103
|
+
:permissions => "r--r--r--")
|
104
|
+
end
|
105
|
+
|
106
|
+
end
|
107
|
+
|
108
|
+
Raval::App.start(:host => "127.0.0.1",
|
109
|
+
:port => 3000,
|
110
|
+
:driver => FakeFTPDriver)
|
data/examples/fog.rb
ADDED
@@ -0,0 +1,183 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
# A FTP proxy to a cloud storage provider. Uses the fog gem to interface
|
4
|
+
# with the storage APIs.
|
5
|
+
#
|
6
|
+
# Any user will be permitted to log in with any password, but they are
|
7
|
+
# sandboxed to their own files. In the real world you will probably want to
|
8
|
+
# use real passwords.
|
9
|
+
#
|
10
|
+
# The try it out:
|
11
|
+
#
|
12
|
+
# 1) create an empty bucket/container with your cloud storage provider
|
13
|
+
# 2) edit the driver_options hash at the bottom of the file
|
14
|
+
# 3) run this:
|
15
|
+
#
|
16
|
+
# ruby -Ilib examples/fog.rb
|
17
|
+
|
18
|
+
require 'ostruct'
|
19
|
+
require 'stringio'
|
20
|
+
require 'raval'
|
21
|
+
require 'fog'
|
22
|
+
|
23
|
+
# the raval gem handles all the FTP protocol details, this driver just acts as
|
24
|
+
# an intermediary between raval and the chosen persistence layer - in this case
|
25
|
+
# that's Fog.
|
26
|
+
class FogFTPDriver
|
27
|
+
|
28
|
+
def initialize(options = {})
|
29
|
+
@options = options
|
30
|
+
end
|
31
|
+
|
32
|
+
def change_dir(path)
|
33
|
+
prefix = scoped_path_with_trailing_slash(path)
|
34
|
+
files = directory.files.all(:prefix => prefix)
|
35
|
+
files.any?
|
36
|
+
end
|
37
|
+
|
38
|
+
def dir_contents(path)
|
39
|
+
prefix = scoped_path_with_trailing_slash(path)
|
40
|
+
directories = []
|
41
|
+
files = []
|
42
|
+
directory.files.all(:prefix => prefix).each do |file|
|
43
|
+
tail = file.key.sub(prefix,"")
|
44
|
+
depth = tail.split("/").size
|
45
|
+
basename = tail.split("/").first
|
46
|
+
if depth == 1 && basename != ".placeholder"
|
47
|
+
files << file_item(basename, file.content_length)
|
48
|
+
elsif depth > 1 && directories.none? { |d| d.name == basename }
|
49
|
+
directories << dir_item(basename)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
(directories + files).sort_by(&:name)
|
53
|
+
end
|
54
|
+
|
55
|
+
def authenticate(user, pass)
|
56
|
+
@user = user
|
57
|
+
true
|
58
|
+
end
|
59
|
+
|
60
|
+
def bytes(path)
|
61
|
+
prefix = scoped_path(path)
|
62
|
+
file = directory.files.head(prefix)
|
63
|
+
file ? file.content_length : nil
|
64
|
+
end
|
65
|
+
|
66
|
+
def modified_time(path)
|
67
|
+
prefix = scoped_path(path)
|
68
|
+
file = directory.files.head(prefix)
|
69
|
+
file ? file.last_modified : nil
|
70
|
+
end
|
71
|
+
|
72
|
+
def get_file(path)
|
73
|
+
prefix = scoped_path(path)
|
74
|
+
file = directory.files.get(prefix)
|
75
|
+
file ? file.body : nil
|
76
|
+
end
|
77
|
+
|
78
|
+
def put_file(path, tmp_path)
|
79
|
+
key = scoped_path(path)
|
80
|
+
File.open(tmp_path, "rb") do |io|
|
81
|
+
directory.files.create(:key => key,
|
82
|
+
:body => io)
|
83
|
+
end
|
84
|
+
File.size(tmp_path)
|
85
|
+
end
|
86
|
+
|
87
|
+
def delete_file(path)
|
88
|
+
prefix = scoped_path(path)
|
89
|
+
file = directory.files.head(prefix)
|
90
|
+
file ? file.destroy : nil
|
91
|
+
end
|
92
|
+
|
93
|
+
def delete_dir(path)
|
94
|
+
prefix = scoped_path_with_trailing_slash(path)
|
95
|
+
files = directory.files.all(:prefix => prefix)
|
96
|
+
|
97
|
+
if files.size == 1 && files.first.key[-12,12] == ".placeholder"
|
98
|
+
file = directory.files.get("#{prefix}.placeholder")
|
99
|
+
file.destroy
|
100
|
+
else
|
101
|
+
false
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def rename(from, to)
|
106
|
+
scoped_from = scoped_path(from)
|
107
|
+
scoped_to = scoped_path(to)
|
108
|
+
old_file = directory.files.head(scoped_from)
|
109
|
+
if old_file
|
110
|
+
storage.copy_object(directory.key,scoped_from,directory.key,scoped_to)
|
111
|
+
old_file.destroy
|
112
|
+
else
|
113
|
+
nil
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
def make_dir(path)
|
118
|
+
prefix = scoped_path_with_trailing_slash(path)
|
119
|
+
files = directory.files.all(:prefix => prefix)
|
120
|
+
if files.any?
|
121
|
+
false # the dir already exists
|
122
|
+
else
|
123
|
+
# upload a placeholder file so the directory "exists"
|
124
|
+
directory.files.create(:key => "#{prefix}.placeholder",
|
125
|
+
:body => ".placeholder")
|
126
|
+
true
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
private
|
131
|
+
|
132
|
+
def storage
|
133
|
+
@storage ||= Fog::Storage.new(@options.reject { |k,_| k == :directory})
|
134
|
+
end
|
135
|
+
|
136
|
+
def directory
|
137
|
+
@directory ||= storage.directories.get(@options[:directory])
|
138
|
+
end
|
139
|
+
|
140
|
+
def dir_item(name)
|
141
|
+
OpenStruct.new(:name => name,
|
142
|
+
:directory => true,
|
143
|
+
:size => 0,
|
144
|
+
:owner => "user",
|
145
|
+
:group => "group",
|
146
|
+
:time => Time.now,
|
147
|
+
:permissions => "r--r--r--")
|
148
|
+
end
|
149
|
+
|
150
|
+
def file_item(name, bytes)
|
151
|
+
OpenStruct.new(:name => name,
|
152
|
+
:directory => false,
|
153
|
+
:size => bytes,
|
154
|
+
:owner => "user",
|
155
|
+
:group => "group",
|
156
|
+
:time => Time.now,
|
157
|
+
:permissions => "r--r--r--")
|
158
|
+
end
|
159
|
+
|
160
|
+
def scoped_path_with_trailing_slash(path)
|
161
|
+
path = scoped_path(path)
|
162
|
+
path += "/" if path[-1,1] != "/"
|
163
|
+
path == "/" ? nil : path
|
164
|
+
end
|
165
|
+
|
166
|
+
def scoped_path(path)
|
167
|
+
path = "" if path == "/"
|
168
|
+
|
169
|
+
File.join("/", @user, path)[1,1024]
|
170
|
+
end
|
171
|
+
|
172
|
+
end
|
173
|
+
|
174
|
+
driver_opts = {:rackspace_username => 'RACKSPACE_USER',
|
175
|
+
:rackspace_api_key => 'RACKSPACE_KEY',
|
176
|
+
:provider => 'Rackspace',
|
177
|
+
:directory => 'RACKSPACE_CONTAINER'}
|
178
|
+
|
179
|
+
Raval::App.start(:host => "127.0.0.1",
|
180
|
+
:port => 3000,
|
181
|
+
:driver => FogFTPDriver,
|
182
|
+
:driver_opts => driver_opts
|
183
|
+
)
|