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
@@ -0,0 +1,30 @@
|
|
1
|
+
module EM::FTPD
|
2
|
+
|
3
|
+
# An eventmachine module for opening a socket for the client to connect
|
4
|
+
# to and send a file
|
5
|
+
#
|
6
|
+
class PassiveSocket < EventMachine::Connection
|
7
|
+
include EM::Deferrable
|
8
|
+
include BaseSocket
|
9
|
+
|
10
|
+
|
11
|
+
def self.start(host, control_server)
|
12
|
+
EventMachine.start_server(host, 0, self) do |conn|
|
13
|
+
control_server.datasocket = conn
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
# stop the server with signature "sig"
|
18
|
+
def self.stop(sig)
|
19
|
+
EventMachine.stop_server(sig)
|
20
|
+
end
|
21
|
+
|
22
|
+
# return the port the server with signature "sig" is listening on
|
23
|
+
#
|
24
|
+
def self.get_port(sig)
|
25
|
+
Socket.unpack_sockaddr_in( EM.get_sockname( sig ) ).first
|
26
|
+
end
|
27
|
+
|
28
|
+
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,343 @@
|
|
1
|
+
require 'socket'
|
2
|
+
require 'stringio'
|
3
|
+
|
4
|
+
require 'eventmachine'
|
5
|
+
require 'em/protocols/line_protocol'
|
6
|
+
|
7
|
+
module EM::FTPD
|
8
|
+
class Server < EM::Connection
|
9
|
+
|
10
|
+
LBRK = "\r\n"
|
11
|
+
|
12
|
+
include EM::Protocols::LineProtocol
|
13
|
+
include Authentication
|
14
|
+
include Directories
|
15
|
+
include Files
|
16
|
+
|
17
|
+
COMMANDS = %w[quit type user retr stor port cdup cwd dele rmd pwd list size
|
18
|
+
syst mkd pass xcup xpwd xcwd xrmd rest allo nlst pasv help
|
19
|
+
noop mode rnfr rnto stru]
|
20
|
+
|
21
|
+
attr_reader :root, :name_prefix
|
22
|
+
attr_accessor :datasocket
|
23
|
+
|
24
|
+
def initialize(driver, *args)
|
25
|
+
if driver.is_a?(Class) && args.empty?
|
26
|
+
@driver = driver.new
|
27
|
+
elsif driver.is_a?(Class)
|
28
|
+
@driver = driver.new *args
|
29
|
+
else
|
30
|
+
@driver = driver
|
31
|
+
end
|
32
|
+
@datasocket = nil
|
33
|
+
@listen_sig = nil
|
34
|
+
super()
|
35
|
+
end
|
36
|
+
|
37
|
+
def post_init
|
38
|
+
@mode = :binary
|
39
|
+
@name_prefix = "/"
|
40
|
+
|
41
|
+
send_response "220 FTP server (em-ftpd) ready"
|
42
|
+
end
|
43
|
+
|
44
|
+
def receive_line(str)
|
45
|
+
cmd, param = parse_request(str)
|
46
|
+
|
47
|
+
# if the command is contained in the whitelist, and there is a method
|
48
|
+
# to handle it, call it. Otherwise send an appropriate response to the
|
49
|
+
# client
|
50
|
+
if COMMANDS.include?(cmd) && self.respond_to?("cmd_#{cmd}".to_sym, true)
|
51
|
+
begin
|
52
|
+
self.__send__("cmd_#{cmd}".to_sym, param)
|
53
|
+
rescue Exception => err
|
54
|
+
puts "#{err.class}: #{err}"
|
55
|
+
puts err.backtrace.join("\n")
|
56
|
+
end
|
57
|
+
else
|
58
|
+
send_response "500 Sorry, I don't understand #{cmd.upcase}"
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
def build_path(filename = nil)
|
65
|
+
if filename && filename[0,1] == "/"
|
66
|
+
path = File.expand_path(filename)
|
67
|
+
elsif filename && filename != '-a'
|
68
|
+
path = File.expand_path("#{@name_prefix}/#{filename}")
|
69
|
+
else
|
70
|
+
path = File.expand_path(@name_prefix)
|
71
|
+
end
|
72
|
+
path.gsub(/\/+/,"/")
|
73
|
+
end
|
74
|
+
|
75
|
+
# split a client's request into command and parameter components
|
76
|
+
def parse_request(data)
|
77
|
+
data.strip!
|
78
|
+
space = data.index(" ")
|
79
|
+
if space
|
80
|
+
cmd = data[0, space]
|
81
|
+
param = data[space+1, data.length - space]
|
82
|
+
param = nil if param.strip.size == 0
|
83
|
+
else
|
84
|
+
cmd = data
|
85
|
+
param = nil
|
86
|
+
end
|
87
|
+
|
88
|
+
[cmd.downcase, param]
|
89
|
+
end
|
90
|
+
|
91
|
+
def close_datasocket
|
92
|
+
if @datasocket
|
93
|
+
@datasocket.close_connection_after_writing
|
94
|
+
@datasocket = nil
|
95
|
+
end
|
96
|
+
|
97
|
+
# stop listening for data socket connections, we have one
|
98
|
+
if @listen_sig
|
99
|
+
PassiveSocket.stop(@listen_sig)
|
100
|
+
@listen_sig = nil
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def cmd_allo(param)
|
105
|
+
send_response "202 Obsolete"
|
106
|
+
end
|
107
|
+
|
108
|
+
# handle the HELP FTP command by sending a list of available commands.
|
109
|
+
def cmd_help(param)
|
110
|
+
send_response "214- The following commands are recognized."
|
111
|
+
commands = COMMANDS
|
112
|
+
str = ""
|
113
|
+
commands.sort.each_slice(3) { |slice|
|
114
|
+
str += " " + slice.join("\t\t") + LBRK
|
115
|
+
}
|
116
|
+
send_response str, true
|
117
|
+
send_response "214 End of list."
|
118
|
+
end
|
119
|
+
|
120
|
+
# the original FTP spec had various options for hosts to negotiate how data
|
121
|
+
# would be sent over the data socket, In reality these days (S)tream mode
|
122
|
+
# is all that is used for the mode - data is just streamed down the data
|
123
|
+
# socket unchanged.
|
124
|
+
#
|
125
|
+
def cmd_mode(param)
|
126
|
+
send_unauthorised and return unless logged_in?
|
127
|
+
send_param_required and return if param.nil?
|
128
|
+
if param.upcase.eql?("S")
|
129
|
+
send_response "200 OK"
|
130
|
+
else
|
131
|
+
send_response "504 MODE is an obsolete command"
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
# handle the NOOP FTP command. This is essentially a ping from the client
|
136
|
+
# so we just respond with an empty 200 message.
|
137
|
+
def cmd_noop(param)
|
138
|
+
send_response "200"
|
139
|
+
end
|
140
|
+
|
141
|
+
# Passive FTP. At the clients request, listen on a port for an incoming
|
142
|
+
# data connection. The listening socket is opened on a random port, so
|
143
|
+
# the host and port is sent back to the client on the control socket.
|
144
|
+
def cmd_pasv(param)
|
145
|
+
send_unauthorised and return unless logged_in?
|
146
|
+
|
147
|
+
# close any existing data socket
|
148
|
+
close_datasocket
|
149
|
+
|
150
|
+
# grab the host/address the current connection is
|
151
|
+
# operating on
|
152
|
+
host = Socket.unpack_sockaddr_in( self.get_sockname ).last
|
153
|
+
|
154
|
+
# open a listening socket on the appropriate host
|
155
|
+
# and on a random port
|
156
|
+
@listen_sig = PassiveSocket.start(host, self)
|
157
|
+
port = PassiveSocket.get_port(@listen_sig)
|
158
|
+
|
159
|
+
# let the client know where to connect
|
160
|
+
p1 = (port / 256).to_i
|
161
|
+
p2 = port % 256
|
162
|
+
|
163
|
+
send_response "227 Entering Passive Mode (" + host.split(".").join(",") + ",#{p1},#{p2})"
|
164
|
+
end
|
165
|
+
|
166
|
+
# Active FTP. An alternative to Passive FTP. The client has a listening socket
|
167
|
+
# open, waiting for us to connect and establish a data socket. Attempt to
|
168
|
+
# open a connection to the host and port they specify and save the connection,
|
169
|
+
# ready for either end to send something down it.
|
170
|
+
def cmd_port(param)
|
171
|
+
send_unauthorised and return unless logged_in?
|
172
|
+
send_param_required and return if param.nil?
|
173
|
+
|
174
|
+
nums = param.split(',')
|
175
|
+
port = nums[4].to_i * 256 + nums[5].to_i
|
176
|
+
host = nums[0..3].join('.')
|
177
|
+
close_datasocket
|
178
|
+
|
179
|
+
puts "connecting to client #{host} on #{port}"
|
180
|
+
@datasocket = ActiveSocket.open(host, port)
|
181
|
+
|
182
|
+
puts "Opened active connection at #{host}:#{port}"
|
183
|
+
send_response "200 Connection established (#{port})"
|
184
|
+
rescue
|
185
|
+
puts "Error opening data connection to #{host}:#{port}"
|
186
|
+
send_response "425 Data connection failed"
|
187
|
+
end
|
188
|
+
|
189
|
+
# handle the QUIT FTP command by closing the connection
|
190
|
+
def cmd_quit(param)
|
191
|
+
send_response "221 Bye"
|
192
|
+
close_datasocket
|
193
|
+
close_connection_after_writing
|
194
|
+
end
|
195
|
+
|
196
|
+
|
197
|
+
# like the MODE and TYPE commands, stru[cture] dates back to a time when the FTP
|
198
|
+
# protocol was more aware of the content of the files it was transferring, and
|
199
|
+
# would sometimes be expected to translate things like EOL markers on the fly.
|
200
|
+
#
|
201
|
+
# These days files are sent unmodified, and F(ile) mode is the only one we
|
202
|
+
# really need to support.
|
203
|
+
def cmd_stru(param)
|
204
|
+
send_param_required and return if param.nil?
|
205
|
+
send_unauthorised and return unless logged_in?
|
206
|
+
if param.upcase.eql?("F")
|
207
|
+
send_response "200 OK"
|
208
|
+
else
|
209
|
+
send_response "504 STRU is an obsolete command"
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
# return the name of the server
|
214
|
+
def cmd_syst(param)
|
215
|
+
send_unauthorised and return unless logged_in?
|
216
|
+
send_response "215 UNIX Type: L8"
|
217
|
+
end
|
218
|
+
|
219
|
+
# like the MODE and STRU commands, TYPE dates back to a time when the FTP
|
220
|
+
# protocol was more aware of the content of the files it was transferring, and
|
221
|
+
# would sometimes be expected to translate things like EOL markers on the fly.
|
222
|
+
#
|
223
|
+
# Valid options were A(SCII), I(mage), E(BCDIC) or LN (for local type). Since
|
224
|
+
# we plan to just accept bytes from the client unchanged, I think Image mode is
|
225
|
+
# adequate. The RFC requires we accept ASCII mode however, so accept it, but
|
226
|
+
# ignore it.
|
227
|
+
def cmd_type(param)
|
228
|
+
send_unauthorised and return unless logged_in?
|
229
|
+
send_param_required and return if param.nil?
|
230
|
+
if param.upcase.eql?("A")
|
231
|
+
send_response "200 Type set to ASCII"
|
232
|
+
elsif param.upcase.eql?("I")
|
233
|
+
send_response "200 Type set to binary"
|
234
|
+
else
|
235
|
+
send_response "500 Invalid type"
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
# send data to the client across the data socket.
|
240
|
+
#
|
241
|
+
# The data socket is NOT guaranteed to be setup by the time this method runs.
|
242
|
+
# If it isn't ready yet, exit the method and try again on the next reactor
|
243
|
+
# tick. This is particularly likely with some clients that operate in passive
|
244
|
+
# mode. They get a message on the control port with the data port details, so
|
245
|
+
# they start up a new data connection AND send they command that will use it
|
246
|
+
# in close succession.
|
247
|
+
#
|
248
|
+
# The data port setup needs to complete a TCP handshake before it will be
|
249
|
+
# ready to use, so it may take a few RTTs after the command is received at
|
250
|
+
# the server before the data socket is ready.
|
251
|
+
#
|
252
|
+
def send_outofband_data(data)
|
253
|
+
wait_for_datasocket do |datasocket|
|
254
|
+
if datasocket.nil?
|
255
|
+
send_response "425 Error establishing connection"
|
256
|
+
return
|
257
|
+
end
|
258
|
+
|
259
|
+
if data.is_a?(Array)
|
260
|
+
data = data.join(LBRK) << LBRK
|
261
|
+
end
|
262
|
+
data = StringIO.new(data) if data.kind_of?(String)
|
263
|
+
|
264
|
+
begin
|
265
|
+
bytes = 0
|
266
|
+
data.each do |line|
|
267
|
+
datasocket.send_data(line)
|
268
|
+
bytes += line.bytesize
|
269
|
+
end
|
270
|
+
send_response "226 Closing data connection, sent #{bytes} bytes"
|
271
|
+
ensure
|
272
|
+
close_datasocket
|
273
|
+
data.close if data.respond_to?(:close)
|
274
|
+
end
|
275
|
+
end
|
276
|
+
end
|
277
|
+
|
278
|
+
# waits for the data socket to be established
|
279
|
+
def wait_for_datasocket(interval = 0.1, &block)
|
280
|
+
if @datasocket.nil? && interval < 25
|
281
|
+
if EM.reactor_running?
|
282
|
+
EventMachine.add_timer(interval) { wait_for_datasocket(interval * 2, &block) }
|
283
|
+
else
|
284
|
+
sleep interval
|
285
|
+
wait_for_datasocket(interval * 2, &block)
|
286
|
+
end
|
287
|
+
return
|
288
|
+
end
|
289
|
+
yield @datasocket
|
290
|
+
end
|
291
|
+
|
292
|
+
# receive a file data from the client across the data socket.
|
293
|
+
#
|
294
|
+
# The data socket is NOT guaranteed to be setup by the time this method runs.
|
295
|
+
# If this happens, exit the method early and try again later. See the method
|
296
|
+
# comments to send_outofband_data for further explanation.
|
297
|
+
#
|
298
|
+
def receive_outofband_data(&block)
|
299
|
+
wait_for_datasocket do |datasocket|
|
300
|
+
if datasocket.nil?
|
301
|
+
send_response "425 Error establishing connection"
|
302
|
+
yield false
|
303
|
+
return
|
304
|
+
end
|
305
|
+
|
306
|
+
# let the client know we're ready to start
|
307
|
+
send_response "150 Data transfer starting"
|
308
|
+
|
309
|
+
datasocket.callback do |data|
|
310
|
+
block.call(data)
|
311
|
+
end
|
312
|
+
end
|
313
|
+
end
|
314
|
+
|
315
|
+
# all responses from an FTP server end with \r\n, so wrap the
|
316
|
+
# send_data callback
|
317
|
+
def send_response(msg, no_linebreak = false)
|
318
|
+
msg += LBRK unless no_linebreak
|
319
|
+
send_data msg
|
320
|
+
end
|
321
|
+
|
322
|
+
def send_param_required
|
323
|
+
send_response "553 action aborted, required param missing"
|
324
|
+
end
|
325
|
+
|
326
|
+
def send_permission_denied
|
327
|
+
send_response "550 Permission denied"
|
328
|
+
end
|
329
|
+
|
330
|
+
def send_action_not_taken
|
331
|
+
send_response "550 Action not taken"
|
332
|
+
end
|
333
|
+
|
334
|
+
def send_illegal_params
|
335
|
+
send_response "553 action aborted, illegal params"
|
336
|
+
end
|
337
|
+
|
338
|
+
def send_unauthorised
|
339
|
+
send_response "530 Not logged in"
|
340
|
+
end
|
341
|
+
|
342
|
+
end
|
343
|
+
end
|
metadata
ADDED
@@ -0,0 +1,158 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: em-ftpd
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- James Healy
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2011-11-18 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: rake
|
16
|
+
requirement: &24543700 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :development
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *24543700
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: rspec
|
27
|
+
requirement: &24542820 !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - ~>
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '2.6'
|
33
|
+
type: :development
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: *24542820
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: em-redis
|
38
|
+
requirement: &24541960 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ! '>='
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: '0'
|
44
|
+
type: :development
|
45
|
+
prerelease: false
|
46
|
+
version_requirements: *24541960
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: guard
|
49
|
+
requirement: &24540940 !ruby/object:Gem::Requirement
|
50
|
+
none: false
|
51
|
+
requirements:
|
52
|
+
- - ! '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
type: :development
|
56
|
+
prerelease: false
|
57
|
+
version_requirements: *24540940
|
58
|
+
- !ruby/object:Gem::Dependency
|
59
|
+
name: guard-process
|
60
|
+
requirement: &24539940 !ruby/object:Gem::Requirement
|
61
|
+
none: false
|
62
|
+
requirements:
|
63
|
+
- - ! '>='
|
64
|
+
- !ruby/object:Gem::Version
|
65
|
+
version: '0'
|
66
|
+
type: :development
|
67
|
+
prerelease: false
|
68
|
+
version_requirements: *24539940
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: guard-bundler
|
71
|
+
requirement: &24539480 !ruby/object:Gem::Requirement
|
72
|
+
none: false
|
73
|
+
requirements:
|
74
|
+
- - ! '>='
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
version: '0'
|
77
|
+
type: :development
|
78
|
+
prerelease: false
|
79
|
+
version_requirements: *24539480
|
80
|
+
- !ruby/object:Gem::Dependency
|
81
|
+
name: guard-rspec
|
82
|
+
requirement: &24538900 !ruby/object:Gem::Requirement
|
83
|
+
none: false
|
84
|
+
requirements:
|
85
|
+
- - ! '>='
|
86
|
+
- !ruby/object:Gem::Version
|
87
|
+
version: '0'
|
88
|
+
type: :development
|
89
|
+
prerelease: false
|
90
|
+
version_requirements: *24538900
|
91
|
+
- !ruby/object:Gem::Dependency
|
92
|
+
name: eventmachine
|
93
|
+
requirement: &24537900 !ruby/object:Gem::Requirement
|
94
|
+
none: false
|
95
|
+
requirements:
|
96
|
+
- - ~>
|
97
|
+
- !ruby/object:Gem::Version
|
98
|
+
version: 1.0.0.beta1
|
99
|
+
type: :runtime
|
100
|
+
prerelease: false
|
101
|
+
version_requirements: *24537900
|
102
|
+
description: Build a custom FTP daemon backed by a datastore of your choice
|
103
|
+
email:
|
104
|
+
- jimmy@deefa.com
|
105
|
+
executables:
|
106
|
+
- em-ftpd
|
107
|
+
extensions: []
|
108
|
+
extra_rdoc_files:
|
109
|
+
- README.markdown
|
110
|
+
- MIT-LICENSE
|
111
|
+
files:
|
112
|
+
- bin/em-ftpd
|
113
|
+
- examples/fake.rb
|
114
|
+
- examples/redis.rb
|
115
|
+
- lib/em-ftpd.rb
|
116
|
+
- lib/em-ftpd/configurator.rb
|
117
|
+
- lib/em-ftpd/directories.rb
|
118
|
+
- lib/em-ftpd/app.rb
|
119
|
+
- lib/em-ftpd/passive_socket.rb
|
120
|
+
- lib/em-ftpd/active_socket.rb
|
121
|
+
- lib/em-ftpd/server.rb
|
122
|
+
- lib/em-ftpd/authentication.rb
|
123
|
+
- lib/em-ftpd/files.rb
|
124
|
+
- lib/em-ftpd/base_socket.rb
|
125
|
+
- lib/em-ftpd/directory_item.rb
|
126
|
+
- Gemfile
|
127
|
+
- README.markdown
|
128
|
+
- MIT-LICENSE
|
129
|
+
homepage: http://github.com/yob/em-ftpd
|
130
|
+
licenses: []
|
131
|
+
post_install_message:
|
132
|
+
rdoc_options:
|
133
|
+
- --title
|
134
|
+
- EM::FTPd Documentation
|
135
|
+
- --main
|
136
|
+
- README.markdown
|
137
|
+
- -q
|
138
|
+
require_paths:
|
139
|
+
- lib
|
140
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
141
|
+
none: false
|
142
|
+
requirements:
|
143
|
+
- - ! '>='
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: 1.9.2
|
146
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
147
|
+
none: false
|
148
|
+
requirements:
|
149
|
+
- - ! '>='
|
150
|
+
- !ruby/object:Gem::Version
|
151
|
+
version: '0'
|
152
|
+
requirements: []
|
153
|
+
rubyforge_project:
|
154
|
+
rubygems_version: 1.8.11
|
155
|
+
signing_key:
|
156
|
+
specification_version: 3
|
157
|
+
summary: An FTP daemon framework
|
158
|
+
test_files: []
|