em-ftpd 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: []