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.
@@ -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: []