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