em-tftp 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (3) hide show
  1. checksums.yaml +7 -0
  2. data/lib/em-tftp.rb +461 -0
  3. metadata +44 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: c5f53c83870189d65638d0835a292998a6fd9711
4
+ data.tar.gz: 26ae9ceac7c4c6443b93b7346e4af8295095c2d5
5
+ SHA512:
6
+ metadata.gz: 3630c5ec775b3a167f1b5e2c201ea0054525daebb45f27451fd5637133b94d5486c7aa4295f39b33ef416a65fe66c805e24d395a223460e42ce839345bf2fd5e
7
+ data.tar.gz: d26eba509ed7d0baf7c644e1cf1575bb7ba5518081d9acdcd827dddf0e0eabd8706d899e422ee6a8071deec71b2401051b4c5676914a5407f2c4c62d7b5db328
data/lib/em-tftp.rb ADDED
@@ -0,0 +1,461 @@
1
+ # encoding: ASCII-8BIT
2
+ # EventMachine-based TFTP implementation
3
+ #
4
+ # References:
5
+ # - RFC 1350 (defines TFTP protocol)
6
+ # - RFC 2347 (negotiatioon of TFTP options)
7
+ # - RFC 2348 (TFTP block size option)
8
+ #
9
+ # Caveats: em-tftp's ReadOnlyFileServer reads an entire file into memory before sending it
10
+ # This will not work well for huge files. But then again, TFTP is not designed for transferring huge files
11
+
12
+ # TODO:
13
+ # - documentation
14
+ # - RDoc
15
+ # - tests
16
+
17
+ require 'eventmachine'
18
+ require 'socket'
19
+
20
+ module TFTP
21
+
22
+ Error = Class.new(Exception)
23
+
24
+ module Protocol
25
+ ERROR_MESSAGES = {
26
+ 0 => "Unknown error",
27
+ 1 => "File not found",
28
+ 2 => "Access violation",
29
+ 3 => "Disk full or allocation exceeded",
30
+ 4 => "Illegal TFTP operation",
31
+ 5 => "Unknown transfer ID",
32
+ 6 => "File already exists",
33
+ 7 => "No such user"}
34
+
35
+ # Used to decode incoming packets
36
+ # TFTP packets have a simple structure: the port number from the encapsulating UDP datagram doubles as a 'transfer ID',
37
+ # allowing multiple, simultaneous transfers betweent the same hosts
38
+ # The first 2 bytes of the payload are an integral 'opcode' from 1-5, in network byte order
39
+ # The opcodes are: Read ReQuest, Write ReQuest, DATA, ACK, and ERROR
40
+ # Depending on the opcode, different fields can follow. All fields are either 16-bit integers in network byte order,
41
+ # or null-terminated strings
42
+ class Packet
43
+ OPCODES = {1 => :rrq, 2 => :wrq, 3 => :data, 4 => :ack, 5 => :error}
44
+
45
+ def initialize(data)
46
+ raise TFTP::Error, "TFTP packet too small (#{data.size} bytes)" if data.size < 4
47
+ raise TFTP::Error, "TFTP packet too large (#{data.size} bytes)" if data.size > 516
48
+ @opcode = OPCODES[data.getbyte(1)]
49
+ if opcode == :data
50
+ @block_no = (data.getbyte(2) << 8) + data.getbyte(3)
51
+ @data = data[4..-1]
52
+ elsif opcode == :rrq || opcode == :wrq
53
+ @filename, _mode = data[2..-1].unpack("Z*Z*") # mode is ignored
54
+ elsif opcode == :error
55
+ @err_code = (data.getbyte(2) << 8) + data.getbyte(3)
56
+ @err_msg = data[4..-2] # don't include null terminator
57
+ if @err_msg.size == 0
58
+ @err_msg = ERROR_MESSAGES[@err_code] || "Unknown error"
59
+ end
60
+ raise TFTP::Error, @err_msg
61
+ elsif opcode == :ack
62
+ @block_no = (data.getbyte(2) << 8) + data.getbyte(3)
63
+ else
64
+ raise TFTP::Error, "Unknown TFTP packet type (opcode #{data.getbyte(1)})"
65
+ end
66
+ end
67
+
68
+ attr_reader :opcode, :filename, :block_no, :data
69
+ end
70
+
71
+ private
72
+
73
+ def send_rrq(addr=@peer_addr, port=@peer_port, filename)
74
+ data = "\0\1" << filename << "\0octet\0"
75
+ send_packet(addr, port, data)
76
+ end
77
+ def send_wrq(addr=@peer_addr, port=@peer_port, filename)
78
+ data = "\0\2" << filename << "\0octet\0"
79
+ send_packet(addr, port, data)
80
+ end
81
+ def send_block(addr=@peer_addr, port=@peer_port, buffer, pos, block_no)
82
+ block = buffer.slice(pos, 512) || ""
83
+ data = "\0\3" << ((block_no >> 8) & 255) << (block_no & 255) << block
84
+ send_packet(data)
85
+ pos + 512
86
+ end
87
+ def send_ack(addr=@peer_addr, port=@peer_port, block_no)
88
+ data = "\0\4" << ((block_no >> 8) & 255) << (block_no & 255)
89
+ send_packet(addr, port, data)
90
+ end
91
+ def send_error(addr=@peer_addr, port=@peer_port, code, msg)
92
+ data = "\0\5" << ((code >> 8) & 255) << (code & 255) << msg << "\0"
93
+ send_packet(addr, port, data)
94
+ end
95
+ def send_packet(addr=@peer_addr, port=@peer_port, data)
96
+ # this appears useless, but is intended to be overridden
97
+ $stderr.puts "Sending: #{data} (#{data[0..3].bytes.to_a.join(',')}) to #{addr}:#{port}" if $DEBUG
98
+ send_datagram(data, addr, port)
99
+ end
100
+ end
101
+
102
+ # An in-progress file transfer operation
103
+ # Holds all the state which a TFTP server/client needs to track for a single transfer
104
+ # Subclasses contain logic specific to client downloads, server downloads, client uploads, server uploads
105
+ class Transfer
106
+ include Protocol
107
+
108
+ BASE_RETRANSMIT_TIMEOUT = 1.5 # seconds
109
+ MAX_RETRANSMIT_TIMEOUT = 12
110
+
111
+ def initialize(connection, peer_addr, peer_port, listener)
112
+ # 'connection' is the UDP socket used for this transfer (actually an EventMachine::Connection object)
113
+ # each transfer uses a newly opened UDP socket, which is closed after the transfer is finished
114
+ # this is because the UDP port number doubles as a TFTP transfer ID
115
+ # so using the same port number for 2 successive requests to the same peer may cause problems
116
+ @connection = connection
117
+ @peer_addr, @peer_port, @listener = peer_addr, peer_port, listener
118
+ @buffer = @block_no = @timer = nil
119
+ @timeout = 1.5
120
+ end
121
+
122
+ attr_reader :peer_addr, :peer_port
123
+ attr_accessor :buffer, :block_no, :timer, :timeout
124
+
125
+ # Abort the file transfer. An optional error message and code can be included.
126
+ # This should be called if the transfer cannot be completed due to a full hard disk, wrong permissions, etc
127
+ #
128
+ # TFTP error codes include:
129
+ #
130
+ # 0 Not defined, see error message (if any).
131
+ # 1 File not found.
132
+ # 2 Access violation.
133
+ # 3 Disk full or allocation exceeded.
134
+ # 4 Illegal TFTP operation.
135
+ # 5 Unknown transfer ID.
136
+ # 6 File already exists.
137
+ # 7 No such user.
138
+ def abort!(code=0, error_msg=Protocol::ERROR_MESSAGES[code])
139
+ stop_timer!
140
+ send_error(code, error_msg || "Unknown error")
141
+ @connection.close_connection_after_writing
142
+ @listener.failed(error_msg || "Unknown error")
143
+ end
144
+
145
+ def error!(error_msg)
146
+ stop_timer!
147
+ @connection.close_connection
148
+ @listener.failed(error_msg)
149
+ end
150
+
151
+ private
152
+
153
+ def stop_timer!
154
+ if @timer
155
+ @timer.cancel
156
+ @timer = nil
157
+ end
158
+ @timeout = BASE_RETRANSMIT_TIMEOUT
159
+ end
160
+ def set_timer!(data)
161
+ @timer = EM::Timer.new(@timeout) do
162
+ @timeout *= 2
163
+ if @timeout <= MAX_RETRANSMIT_TIMEOUT
164
+ send_packet(data)
165
+ else
166
+ abort!(0, "Connection timed out")
167
+ end
168
+ end
169
+ end
170
+
171
+ def finished!
172
+ stop_timer!
173
+ @connection.close_connection
174
+ @listener.completed
175
+ end
176
+
177
+ def send_packet(addr=@peer_addr, port=@peer_port, packet_data)
178
+ $stderr.puts "Sending: #{packet_data} (#{packet_data[0..3].bytes.to_a.join(',')}) to #{addr}:#{port}" if $DEBUG
179
+ @connection.send_datagram(packet_data, addr, port)
180
+ set_timer!(packet_data) unless packet_data.start_with? "\0\5" # no timeout and retransmit for error packets
181
+ end
182
+
183
+ def rrq(packet, port)
184
+ # a transfer has already started and is not yet finished
185
+ # a RRQ/WRQ shouldn't arrive at this time
186
+ abort!(4, "Received unexpected TFTP RRQ packet")
187
+ end
188
+ def wrq(packet, port)
189
+ abort!(4, "Received unexpected TFTP WRQ packet")
190
+ end
191
+ end
192
+
193
+ class Receive < Transfer
194
+ # Receive a file from the peer
195
+ def initialize(connection, peer_addr, peer_port, listener)
196
+ super(connection, peer_addr, peer_port, listener)
197
+ @buffer = ""
198
+ @block_no = 1
199
+ end
200
+
201
+ def data(packet, port)
202
+ if packet.block_no == @block_no
203
+ stop_timer!
204
+ if @peer_port.nil?
205
+ @peer_port = port
206
+ end
207
+ if packet.data.size > 0
208
+ @listener.received_block(packet.data)
209
+ end
210
+ if packet.data.size < 512
211
+ finished!
212
+ else
213
+ send_ack(@block_no)
214
+ @block_no += 1
215
+ end
216
+ end
217
+ end
218
+ def ack(packet, port)
219
+ abort!(4, "Received unexpected TFTP ACK packet while receiving file")
220
+ end
221
+ end
222
+
223
+ class ClientReceive < Receive
224
+ def initialize(connection, peer_addr, listener, filename)
225
+ super(connection, peer_addr, nil, listener)
226
+ send_rrq(peer_addr, 69, filename)
227
+ end
228
+ end
229
+
230
+ class ServerReceive < Receive
231
+ def initialize(connection, peer_addr, peer_port, listener)
232
+ super(connection, peer_addr, peer_port, listener)
233
+ send_ack(0)
234
+ end
235
+ end
236
+
237
+ class Send < Transfer
238
+ # Send a file to the peer
239
+ # Note that the file data does not necessarily have to originate from the filesystem;
240
+ # it could be cached in memory or generated dynamically
241
+ def initialize(connection, peer_addr, peer_port, listener, file_data)
242
+ super(connection, peer_addr, peer_port, listener)
243
+ @buffer = file_data
244
+ end
245
+
246
+ def ack(packet, port)
247
+ if packet.block_no == @block_no
248
+ stop_timer!
249
+ if @peer_port.nil?
250
+ @peer_port = nil
251
+ end
252
+ @block_no += 1
253
+ if @buffer.size <= 512
254
+ @pos = send_block(@buffer, @pos, @block_no)
255
+ finished!
256
+ else
257
+ @pos = send_block(@buffer, @pos, @block_no)
258
+ end
259
+ end
260
+ end
261
+ def data(packet, port)
262
+ abort!(4, "Received unexpected TFTP DATA packet while sending file")
263
+ end
264
+ end
265
+
266
+ class ClientSend < Send
267
+ def initialize(connection, peer_addr, listener, filename, file_data)
268
+ super(connection, peer_addr, nil, listener, file_data)
269
+ # we need to send a WRQ and get an ACK before sending
270
+ @block_no = 0
271
+ @pos = 0
272
+ send_wrq(peer_addr, 69, filename)
273
+ end
274
+ end
275
+
276
+ class ServerSend < Send
277
+ def initialize(connection, peer_addr, peer_port, listener, file_data)
278
+ super(connection, peer_addr, peer_port, listener, file_data)
279
+ # we have already received a RRQ, we can send to the peer immediately
280
+ @block_no = 1
281
+ @pos = send_block(@buffer, 0, @block_no)
282
+ end
283
+ end
284
+
285
+ def self.ListeningConnection(listener_klass)
286
+ # create a subclass of ListeningConnection which uses a specific type of listener
287
+ # this is necessary because when opening a socket, EM does not take a connection OBJECT argument, but a connection CLASS
288
+ Class.new(TFTP::ListeningConnection).tap { |c| c.instance_variable_set(:@listener_klass, listener_klass) }
289
+ end
290
+
291
+ # UDP socket (really EM::Connection) on TFTP server which waits for incoming RRQ/WRQ
292
+ # When an incoming transfer request arrives, calls get/put on its listener
293
+ # and starts the transfer if get passes true,<file data> (or put passes true) to the block
294
+ # If the data must be read from hard disk, EM.defer { File.read(...) } is recommended,
295
+ # so as not to block the EM reactor from handling events
296
+ class ListeningConnection < EM::Connection
297
+ include Protocol
298
+
299
+ def listener
300
+ self.class.instance_eval { @listener_klass }
301
+ end
302
+
303
+ def receive_data(data)
304
+ peer_port, peer_addr = Socket.unpack_sockaddr_in(get_peername)
305
+ $stderr.puts "Received: #{data} (#{data[0..3].bytes.to_a.join(',')}) from #{peer_addr}:#{peer_port}" if $DEBUG
306
+ packet = Packet.new(data.encode!(Encoding::BINARY))
307
+ send(packet.opcode, peer_addr, peer_port, packet)
308
+ rescue TFTP::Error
309
+ if listener.respond_to?(:error)
310
+ listener.error($!.message)
311
+ end
312
+ end
313
+
314
+ private
315
+
316
+ def rrq(addr, port, packet)
317
+ listener.get(addr, port, packet.filename) do |aye_nay, str_data|
318
+ if aye_nay
319
+ connection = EM.open_datagram_socket('0.0.0.0', 0, TFTP::TransferConnection)
320
+ connection.transfer = ServerSend.new(connection, addr, port, listener.new, str_data)
321
+ else
322
+ send_error(addr, port, 0, str_data || "Denied")
323
+ end
324
+ end
325
+ end
326
+
327
+ def wrq(addr, port, packet)
328
+ listener.put(addr, port, packet.filename) do |aye_nay, str_data|
329
+ if aye_nay
330
+ connection = EM.open_datagram_socket('0.0.0.0', 0, TFTP::TransferConnection)
331
+ connection.transfer = ServerReceive.new(connection, addr, port, listener.new)
332
+ else
333
+ send_error(addr, port, 0, "Denied")
334
+ end
335
+ end
336
+ end
337
+
338
+ def data(addr, port, packet)
339
+ send_error(addr, port, 5, "Unknown transfer ID")
340
+ end
341
+ def ack(addr, port, packet)
342
+ # It's not a good idea to send an error back for unexpected ACK;
343
+ # some TFTP clients may send an "eager" ACK to try to make file come faster
344
+ end
345
+ end
346
+
347
+ # UDP socket (really EM::Connection) which is being used for an in-progress transfer
348
+ class TransferConnection < EM::Connection
349
+ include Protocol
350
+
351
+ attr_accessor :transfer
352
+
353
+ def receive_data(data)
354
+ peer_port, peer_addr = Socket.unpack_sockaddr_in(get_peername)
355
+ $stderr.puts "Received: #{data} (#{data[0..3].bytes.to_a.join(',')}) from #{peer_addr}:#{peer_port}" if $DEBUG
356
+ packet = Packet.new(data.encode!(Encoding::BINARY))
357
+ if transfer && peer_addr == transfer.peer_addr && (peer_port == transfer.peer_port || transfer.peer_port.nil?)
358
+ transfer.send(packet.opcode, packet, peer_port)
359
+ end
360
+ rescue TFTP::Error
361
+ transfer.error!($!.message)
362
+ end
363
+ end
364
+
365
+ # Listeners
366
+
367
+ class ClientUploader
368
+ def initialize(&block)
369
+ @callback = block
370
+ end
371
+ def completed
372
+ @callback.call(true, nil)
373
+ end
374
+ def failed(error_msg)
375
+ @callback.call(false, error_msg)
376
+ end
377
+ end
378
+
379
+ class ClientDownloader
380
+ def initialize(&block)
381
+ @callback = block
382
+ @buffer = ""
383
+ end
384
+ def received_block(block)
385
+ @buffer << block
386
+ end
387
+ def completed
388
+ @callback.call(true, @buffer)
389
+ end
390
+ def failed(error_msg)
391
+ @callback.call(false, error_msg)
392
+ end
393
+ end
394
+
395
+ def self.ReadOnlyFileServer(base_dir)
396
+ Class.new(ReadOnlyFileServer).tap { |c| c.instance_variable_set(:@base_dir, base_dir) }
397
+ end
398
+
399
+ class ReadOnlyFileServer
400
+ def self.get(addr, port, filename, &block)
401
+ filename.slice!(0) if filename.start_with?('/')
402
+ begin
403
+ path = File.join(@base_dir, filename)
404
+ if File.exist?(path)
405
+ EventMachine.defer(proc { File.binread(path) }, proc { |file_data| block.call(true, file_data) })
406
+ else
407
+ block.call(false, "File not found")
408
+ end
409
+ rescue
410
+ block.call(false, $!.message)
411
+ end
412
+ end
413
+
414
+ def self.put(addr, port, filename)
415
+ yield false
416
+ end
417
+
418
+ def completed
419
+ # log?
420
+ end
421
+ def failed(error_msg)
422
+ # log?
423
+ end
424
+
425
+ private
426
+
427
+ def base_dir
428
+ self.class.instance_eval { @base_dir }
429
+ end
430
+ end
431
+ end
432
+
433
+ module EventMachine
434
+ class << self
435
+ def start_tftp_server(server='0.0.0.0', port=69, listener_klass)
436
+ if !listener_klass.is_a?(Class)
437
+ if listener_klass.is_a?(Module)
438
+ listener_klass = Class.new.tap { include listener_klass }
439
+ else
440
+ raise ArgumentError, "Expected a class which defines callback methods like #received_block, #completed, and #failed; got #{listener_klass.class} instead"
441
+ end
442
+ end
443
+
444
+ EM.open_datagram_socket(server, port, TFTP::ListeningConnection(listener_klass))
445
+ end
446
+
447
+ def tftp_get(server, port=69, filename, &callback)
448
+ conn = EM.open_datagram_socket('0.0.0.0', 0, TFTP::TransferConnection)
449
+ conn.transfer = TFTP::ClientReceive.new(conn, server, TFTP::ClientDownloader.new(&callback), filename)
450
+ end
451
+
452
+ def tftp_put(server, port=69, filename, file_data, &callback)
453
+ conn = EM.open_datagram_socket('0.0.0.0', 0, TFTP::TransferConnection)
454
+ if file_data.is_a?(IO)
455
+ EventMachine.defer(proc { file_data.read }, proc { |data| conn.transfer = TFTP::ClientSend.new(conn, server, port, TFTP::ClientUploader.new(&callback), filename, data) })
456
+ else
457
+ conn.transfer = TFTP::ClientSend.new(conn, server, TFTP::ClientUploader.new(&callback), filename, file_data)
458
+ end
459
+ end
460
+ end
461
+ end
metadata ADDED
@@ -0,0 +1,44 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: em-tftp
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Alex Dowad
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-06-14 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: Ruby EventMachine-based TFTP implementation (both server and client)
14
+ email: alexinbeijing@gmail.com
15
+ executables: []
16
+ extensions: []
17
+ extra_rdoc_files: []
18
+ files:
19
+ - lib/em-tftp.rb
20
+ homepage: http://github.com/alexdowad/em-tftp
21
+ licenses:
22
+ - None (Public Domain)
23
+ metadata: {}
24
+ post_install_message:
25
+ rdoc_options: []
26
+ require_paths:
27
+ - lib
28
+ required_ruby_version: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ required_rubygems_version: !ruby/object:Gem::Requirement
34
+ requirements:
35
+ - - ">="
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ requirements: []
39
+ rubyforge_project:
40
+ rubygems_version: 2.2.2
41
+ signing_key:
42
+ specification_version: 4
43
+ summary: Ruby EventMachine TFTP implementation
44
+ test_files: []