tftpplus 0.2

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 (6) hide show
  1. data/ChangeLog +47 -0
  2. data/README +21 -0
  3. data/bin/tftp_client.rb +112 -0
  4. data/lib/net/tftp+.rb +796 -0
  5. data/test/test.rb +58 -0
  6. metadata +49 -0
data/ChangeLog ADDED
@@ -0,0 +1,47 @@
1
+ 2006-12-08 msoulier
2
+ * Fixed handling of remote TID.
3
+ * Added tar task to Rakefile.
4
+
5
+ 2006-10-10 msoulier
6
+ * Added site, and Rakefile entry to push site.
7
+
8
+ 2006-10-10 01:46 msoulier
9
+
10
+ * trunk/ChangeLog, trunk/README, trunk/doc,
11
+ trunk/lib/net/tftp+.rb: Adding Rakefile
12
+
13
+ 2006-10-06 01:35 msoulier
14
+
15
+ * trunk/lib/net/tftp+.rb: Optimizing Tftp object dispatch
16
+
17
+ 2006-10-04 01:27 msoulier
18
+
19
+ * trunk/lib/net/tftp+.rb: minor method rename
20
+
21
+ 2006-09-25 02:11 msoulier
22
+
23
+ * trunk/lib/net/tftp+.rb: Added timeouts on recvfrom
24
+
25
+ 2006-09-24 02:39 msoulier
26
+
27
+ * trunk/lib/net/tftp+.rb: Updated debugging output
28
+
29
+ 2006-09-24 02:31 msoulier
30
+
31
+ * trunk/lib/net/tftp+.rb, trunk/share, trunk/share/tftp_client.rb,
32
+ trunk/test/test.rb: Moved client code into its own sample client.
33
+
34
+ 2006-09-22 16:48 msoulier
35
+
36
+ * trunk/lib/net/tftp+.rb: Pulling hardcoded test
37
+
38
+ 2006-09-22 16:44 msoulier
39
+
40
+ * doc, lib, test, trunk/doc, trunk/lib, trunk/test: Setting up
41
+ branch structure.
42
+
43
+ 2006-09-22 16:42 msoulier
44
+
45
+ * doc, doc/rfc1350.txt, doc/rfc2347.txt, doc/rfc2348.txt, lib,
46
+ lib/net, lib/net/tftp+.rb, test, test/test.rb: Initial import.
47
+
data/README ADDED
@@ -0,0 +1,21 @@
1
+ About Tftpplus:
2
+ ===============
3
+
4
+ A new tftp library for clients and servers that supports RFCs 1350, 2347 and
5
+ 2348 (ie. variable block sizes).
6
+
7
+ Release Notes:
8
+ ==============
9
+
10
+ About version 0.2:
11
+ ==================
12
+
13
+ - Fixed handling of remote TID.
14
+
15
+ About version 0.1:
16
+ ==================
17
+
18
+ - Added timeouts on recvfrom
19
+ - Updated debugging output
20
+ - Moved client code into its own sample client.
21
+ - Client functioning with support for variable block sizes.
@@ -0,0 +1,112 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $:.unshift File.join(File.dirname(__FILE__), "..", "lib")
4
+ require 'net/tftp+'
5
+ require 'optparse'
6
+ require 'ostruct'
7
+ require 'logger'
8
+
9
+ LogLevel = Logger::INFO
10
+
11
+ $tftplog = Logger.new($stderr)
12
+ $tftplog.level = LogLevel
13
+ $log = $tftplog
14
+
15
+ def parse_args
16
+ # Set up defaults
17
+ options = OpenStruct.new
18
+ options.filename = nil
19
+ options.host = nil
20
+ options.debug = false
21
+ options.blksize = 512
22
+ options.port = 69
23
+
24
+ banner =<<EOF
25
+ Usage: tftp_client <options>
26
+ EOF
27
+ opts = nil
28
+ begin
29
+ $log.debug("client") { "Parsing command line arguments" }
30
+ opts = OptionParser.new do |opts|
31
+ opts.banner = banner
32
+
33
+ opts.on('-f', '--filename=MANDATORY', 'Remote filename') do |f|
34
+ options.filename = f
35
+ $log.debug('client') { "filename is #{f}" }
36
+ end
37
+ opts.on('-h', '--host=MANDATORY', 'Remote host or IP address') do |h|
38
+ options.host = h
39
+ $log.debug('client') { "host is #{h}" }
40
+ end
41
+ opts.on('-p', '--port=', 'Remote port to use (default: 69)') do |p|
42
+ options.port = p.to_i
43
+ $log.debug('client') { "port is #{p}" }
44
+ end
45
+ opts.on('-d', '--debug', 'Debugging output on') do |d|
46
+ options.debug = d
47
+ $log.level = Logger::DEBUG
48
+ $log.debug('client') { "Debug output requested" }
49
+ end
50
+ opts.on('-b', '--blksize=', 'Blocksize option: 8-65536 bytes') do |b|
51
+ options.blksize = b.to_i
52
+ $log.debug('client') { "blksize is #{b}" }
53
+ end
54
+ opts.on_tail('-h', '--help', 'Show this message') do
55
+ puts opts
56
+ exit
57
+ end
58
+ end.parse!
59
+
60
+ unless options.filename and options.host
61
+ raise OptionParser::InvalidOption,
62
+ "Both --host and --filename are required"
63
+ end
64
+ #unless options.blksize =~ /^\d+/
65
+ # raise OptionParser::InvalidOption,
66
+ # "blksize must be an integer"
67
+ #end
68
+ unless options.blksize >= 8 and options.blksize <= 65536
69
+ raise OptionParser::InvalidOption,
70
+ "blksize can only be between 8 and 65536 bytes"
71
+ end
72
+ unless options.port > 0 and options.port < 65537
73
+ raise OptionParser::InvalidOption,
74
+ "port must be positive integer between 1 and 65536"
75
+ end
76
+ rescue Exception => details
77
+ $stderr.puts details.to_s
78
+ $stderr.puts opts
79
+ exit 1
80
+ end
81
+
82
+ return options
83
+ end
84
+
85
+ def main
86
+ options = parse_args
87
+
88
+ size = 0
89
+ start = Time.now
90
+ $log.info('client') { "Starting download of #{options.filename} from #{options.host}" }
91
+ $log.info('client') { "Options: blksize = #{options.blksize}" }
92
+
93
+ client = TftpClient.new(options.host, options.port)
94
+ tftp_opts = { :blksize => options.blksize.to_i }
95
+ client.download(options.filename, options.filename, tftp_opts) do |pkt|
96
+ size += pkt.data.length
97
+ $log.debug('client') { "Downloaded #{size} bytes" }
98
+ end
99
+
100
+ finish = Time.now
101
+ duration = finish - start
102
+
103
+ $log.info('client') { "" }
104
+ $log.info('client') { "Started: #{start}" }
105
+ $log.info('client') { "Finished: #{finish}" }
106
+ $log.info('client') { "Duration: #{duration}" }
107
+ $log.info('client') { "Downloaded #{size} bytes in #{duration} seconds" }
108
+ $log.info('client') { "Throughput: #{(size/duration)*8} bps" }
109
+ $log.info('client') { " #{(size/duration)*8 / 1024} kbps" }
110
+ end
111
+
112
+ main
data/lib/net/tftp+.rb ADDED
@@ -0,0 +1,796 @@
1
+ # A Ruby library for Trivial File Transfer Protocol.
2
+ # Supports the following RFCs
3
+ # RFC 1350 - THE TFTP PROTOCOL (REVISION 2)
4
+ # RFC 2347 - TFTP Option Extension
5
+ # RFC 2348 - TFTP Blocksize Option
6
+ # Currently, the TftpServer class is not functional.
7
+ # The TftpClient class works fine. Let me know if this is not true.
8
+
9
+ require 'socket'
10
+ require 'timeout'
11
+ require 'resolv'
12
+
13
+ # Todo
14
+ # - properly handle decoding of options ala rfc 2347
15
+ # - use maxdups
16
+ # - use socket timeouts
17
+ # - implement variable block-sizes
18
+
19
+ MinBlkSize = 8
20
+ DefBlkSize = 512
21
+ MaxBlkSize = 65536
22
+ SockTimeout = 5
23
+ MaxDups = 20
24
+ Assertions = true
25
+ MaxRetry = 5
26
+
27
+ # This class is a Nil logging device. It catches all of the logger calls and
28
+ # does nothing with them. It is up to the client to provide a real logger
29
+ # and assign it to $tftplog.
30
+ class TftpNilLogger
31
+ def method_missing(*args)
32
+ # do nothing
33
+ end
34
+ end
35
+
36
+ # This global is the logger used by the library. By default it is an instance
37
+ # of TftpNilLogger, which does nothing. Replace it with a logger if you want
38
+ # one.
39
+ $tftplog = TftpNilLogger.new
40
+
41
+ class TftpError < RuntimeError
42
+ end
43
+
44
+ # This function is a custom assertion in the library to catch unsupported
45
+ # states and types. If the assertion fails, msg is raised in a TftpError
46
+ # exception.
47
+ def tftpassert(msg, &code)
48
+ if not code.call and Assertions
49
+ raise TftpError, "Assertion Failed: #{msg}", caller
50
+ end
51
+ end
52
+
53
+ # This class is the root of all TftpPacket classes in the library. It should
54
+ # not be instantiated directly. It exists to provide code sharing to the child
55
+ # classes.
56
+ class TftpPacket
57
+ attr_accessor :opcode, :buffer
58
+
59
+ # Class constructor. This class and its children take no parameters. A
60
+ # client is expected to set instance variables after instantiation.
61
+ def initialize
62
+ @opcode = 0
63
+ @buffer = nil
64
+ @options = {}
65
+ end
66
+
67
+ # Abstract method, must be implemented in all child classes.
68
+ def encode
69
+ raise NotImplementedError
70
+ end
71
+
72
+ # Abstract method, must be implemented in all child classes.
73
+ def decode
74
+ raise NotImplementedError
75
+ end
76
+
77
+ # This is a setter for the options hash. It ensures that the keys are
78
+ # Symbols and that the values are strings. You can pass in a non-String
79
+ # value as long as the .to_s method returns a good value.
80
+ def options=(opts)
81
+ myopts = {}
82
+ opts.each do |key, val|
83
+ $tftplog.debug('tftp+') { "looping on key #{key}, val #{val}" }
84
+ $tftplog.debug('tftp+') { "class of key is #{key.class}" }
85
+ tftpassert("options keys must be symbols") { key.class == Symbol }
86
+ myopts[key.to_s] = val.to_s
87
+ end
88
+ @options = myopts
89
+ end
90
+
91
+ # A getter for the options hash.
92
+ def options
93
+ return @options
94
+ end
95
+
96
+ protected
97
+
98
+ # This method takes the portion of the buffer containing the options and
99
+ # decodes it, returning a hash of the option name/value pairs, with the
100
+ # keys as Symbols and the values as Strings.
101
+ def decode_options(buffer)
102
+ # We need to variably decode the buffer. The buffer here is only that
103
+ # part of the original buffer containing options. We will decode the
104
+ # options here and return an options array.
105
+ nulls = 0
106
+ format = ""
107
+ # Count the nulls in the buffer, each one terminates a string.
108
+ buffer.collect do |c|
109
+ if c.to_i == 0
110
+ format += "Z*Z*"
111
+ end
112
+ end
113
+ struct = buffer.unpack(format)
114
+
115
+ unless struct.length % 2 == 0
116
+ raise TftpError, "packet with odd number of option/value pairs"
117
+ end
118
+
119
+ while not struct.empty?
120
+ name = struct.shift
121
+ value = struct.shift
122
+ options[name.to_sym] = value
123
+ $tftplog.debug('tftp+') { "decoded option #{name} with value #{value}" }
124
+ end
125
+ return options
126
+ end
127
+ end
128
+
129
+ # This class is a parent class for the RRQ and WRQ packets, as they share a
130
+ # lot of code.
131
+ # 2 bytes string 1 byte string 1 byte
132
+ # -----------------------------------------------
133
+ # RRQ/ | 01/02 | Filename | 0 | Mode | 0 |
134
+ # WRQ -----------------------------------------------
135
+ # +-------+---~~---+---+---~~---+---+---~~---+---+---~~---+---+
136
+ # | opc |filename| 0 | mode | 0 | blksize| 0 | #octets| 0 |
137
+ # +-------+---~~---+---+---~~---+---+---~~---+---+---~~---+---+
138
+ class TftpPacketInitial < TftpPacket
139
+ attr_accessor :filename, :mode
140
+
141
+ def initialize
142
+ super()
143
+ @filename = nil
144
+ @mode = nil
145
+ end
146
+
147
+ # Encode of the packet based on the instance variables. Both the filename
148
+ # and mode instance variables must be set or an exception will be thrown.
149
+ def encode
150
+ unless @opcode and @filename and @mode
151
+ raise ArgumentError, "Required arguments missing."
152
+ end
153
+
154
+ datalist = []
155
+
156
+ format = "n"
157
+ format += "a#{@filename.length}x"
158
+ datalist.push @opcode
159
+ datalist.push @filename
160
+
161
+ case @mode
162
+ when "octet"
163
+ format += "a5"
164
+ else
165
+ raise ArgumentError, "Unsupported mode: #{kwargs[:mode]}"
166
+ end
167
+ datalist.push @mode
168
+
169
+ format += "x"
170
+
171
+ @options.each do |key, value|
172
+ format += "a#{key.length}x"
173
+ format += "a#{value.length}x"
174
+ datalist.push key
175
+ datalist.push value
176
+ end
177
+
178
+ @buffer = datalist.pack(format)
179
+ return self
180
+ end
181
+
182
+ # Decode the packet based on the contents of the buffer instance variable.
183
+ # It populates the filename and mode instance variables.
184
+ def decode
185
+ unless @buffer
186
+ raise ArgumentError, "Can't decode, buffer is empty."
187
+ end
188
+ struct = @buffer.unpack("nZ*Z*")
189
+ unless struct[0] == 1 or struct[0] == 2
190
+ raise TftpError, "opcode #{struct[0]} is not a RRQ or WRQ!"
191
+ end
192
+ @filename = struct[1]
193
+ unless @filename.length > 0
194
+ raise TftpError, "filename is the null string"
195
+ end
196
+ @mode = struct[2]
197
+ unless valid_mode? @mode
198
+ raise TftpError, "mode #{@mode} is not valid"
199
+ end
200
+
201
+ # We need to find the point at which the opcode, filename and mode
202
+ # have ended and the options begin.
203
+ offset = 0
204
+ nulls = []
205
+ @buffer.each_byte do |c|
206
+ nulls.push offset if c == 0
207
+ offset += 1
208
+ end
209
+ # There should be at least 3, the 0 in the opcode, the terminator for
210
+ # the filename, and the terminator for the mode. If there are more,
211
+ # then there are options.
212
+ if nulls.length < 3
213
+ raise TftpError, "Failed to parse nulls looking for options"
214
+ elsif nulls.length > 3
215
+ lower_bound = nulls[2] + 1
216
+ @options = decode_options(@buffer[lower_bound..-1])
217
+ end
218
+
219
+ return self
220
+ end
221
+
222
+ protected
223
+
224
+ # This method is a boolean validator that returns true if the blocksize
225
+ # passed is valid, and false otherwise.
226
+ def valid_blocksize?(blksize)
227
+ blksize = blksize.to_i
228
+ if blksize >= 8 and blksize <= 65464
229
+ return true
230
+ else
231
+ return false
232
+ end
233
+ end
234
+
235
+ # This method is a boolean validator that returns true of the mode passed
236
+ # is valid, and false otherwise. The modes of 'netascii', 'octet' and
237
+ # 'mail' are valid, even though only 'octet' is currently implemented.
238
+ def valid_mode?(mode)
239
+ case mode
240
+ when "netascii", "octet", "mail"
241
+ return true
242
+ else
243
+ return false
244
+ end
245
+ end
246
+ end
247
+
248
+ # The RRQ packet to request a download.
249
+ class TftpPacketRRQ < TftpPacketInitial
250
+ def initialize
251
+ super()
252
+ @opcode = 1
253
+ end
254
+ end
255
+
256
+ # The WRQ packet to request an upload.
257
+ class TftpPacketWRQ < TftpPacketInitial
258
+ def initialize
259
+ super()
260
+ @opcode = 2
261
+ end
262
+ end
263
+
264
+ # 2 bytes 2 bytes n bytes
265
+ # ---------------------------------
266
+ # DATA | 03 | Block # | Data |
267
+ # ---------------------------------
268
+ class TftpPacketDAT < TftpPacket
269
+ attr_accessor :data, :buffer, :blocknumber
270
+
271
+ def initialize
272
+ super()
273
+ @opcode = 3
274
+ @blocknumber = 0
275
+ @data = nil
276
+ @buffer = nil
277
+ end
278
+
279
+ def encode
280
+ unless @opcode and @blocknumber and @data
281
+ raise ArgumentError, "Required fields missing!"
282
+ end
283
+ # FIXME - check block size
284
+ #@buffer = [@opcode, @blocknumber, @data].pack('nnC#{@data.length}')
285
+ @buffer = [@opcode, @blocknumber].pack('nn')
286
+ @buffer += @data
287
+ return self
288
+ end
289
+
290
+ def decode
291
+ unless @buffer
292
+ raise ArgumentError, "Can't decode, buffer is empty."
293
+ end
294
+ struct = @buffer[0..3].unpack('nn')
295
+ unless struct[0] == 3
296
+ raise ArgumentError, "opcode #{struct[0]} is not a DAT!"
297
+ end
298
+ @blocknumber = struct[1]
299
+ @data = @buffer[4..-1]
300
+ return self
301
+ end
302
+ end
303
+
304
+ # 2 bytes 2 bytes
305
+ # -------------------
306
+ # ACK | 04 | Block # |
307
+ # --------------------
308
+ class TftpPacketACK < TftpPacket
309
+ attr_accessor :blocknumber, :buffer
310
+
311
+ def initialize
312
+ super()
313
+ @opcode = 4
314
+ @blocknumber = 0
315
+ @buffer = nil
316
+ end
317
+
318
+ def encode
319
+ unless @blocknumber
320
+ raise ArgumentError, "blocknumber required"
321
+ end
322
+ @buffer = [@opcode, @blocknumber].pack('nn')
323
+ return self
324
+ end
325
+
326
+ def decode
327
+ unless @buffer
328
+ raise ArgumentError, "Can't decode, buffer is empty."
329
+ end
330
+ struct = @buffer.unpack('nn')
331
+ unless struct[0] == 4
332
+ raise ArgumentError, "opcode #{struct[0]} is not an ACK!"
333
+ end
334
+ @blocknumber = struct[1]
335
+ return self
336
+ end
337
+ end
338
+
339
+ # 2 bytes 2 bytes string 1 byte
340
+ # ----------------------------------------
341
+ # ERROR | 05 | ErrorCode | ErrMsg | 0 |
342
+ # ----------------------------------------
343
+ # Error Codes
344
+ #
345
+ # Value Meaning
346
+ #
347
+ # 0 Not defined, see error message (if any).
348
+ # 1 File not found.
349
+ # 2 Access violation.
350
+ # 3 Disk full or allocation exceeded.
351
+ # 4 Illegal TFTP operation.
352
+ # 5 Unknown transfer ID.
353
+ # 6 File already exists.
354
+ # 7 No such user.
355
+ # 8 Failed negotiation
356
+ class TftpPacketERR < TftpPacket
357
+ attr_reader :extended_errmsg
358
+ attr_accessor :errorcode, :errmsg, :buffer
359
+ ErrMsgs = [
360
+ 'Not defined, see error message (if any).',
361
+ 'File not found.',
362
+ 'Access violation.',
363
+ 'Disk full or allocation exceeded.',
364
+ 'Illegal TFTP operation.',
365
+ 'Unknown transfer ID.',
366
+ 'File already exists.',
367
+ 'No such user.',
368
+ 'Failed negotiation.'
369
+ ]
370
+
371
+ def initialize
372
+ super()
373
+ @opcode = 5
374
+ @errorcode = 0
375
+ @errmsg = nil
376
+ @extended_errmsg = nil
377
+ @buffer = nil
378
+ end
379
+
380
+ def encode
381
+ unless @opcode and @errorcode
382
+ raise ArgumentError, "Required params missing."
383
+ end
384
+ @errmsg = ErrMsgs[@errorcode] unless @errmsg
385
+ format = 'nn' + "a#{@errmsg.length}" + 'x'
386
+ @buffer = [@opcode, @errorcode, @errmsg].pack(format)
387
+ return self
388
+ end
389
+
390
+ def decode
391
+ unless @buffer
392
+ raise ArgumentError, "Can't decode, buffer is empty."
393
+ end
394
+ struct = @buffer.unpack("nnZ*")
395
+ unless struct[0] == 5
396
+ raise ArgumentError, "opcode #{struct[0]} is not an ERR"
397
+ end
398
+ @errorcode = struct[1]
399
+ @errmsg = struct[2]
400
+ @extended_errmsg = ErrMsgs[@errorcode]
401
+ return self
402
+ end
403
+
404
+ end
405
+
406
+ # +-------+---~~---+---+---~~---+---+---~~---+---+---~~---+---+
407
+ # | opc | opt1 | 0 | value1 | 0 | optN | 0 | valueN | 0 |
408
+ # +-------+---~~---+---+---~~---+---+---~~---+---+---~~---+---+
409
+ class TftpPacketOACK < TftpPacket
410
+ def initialize
411
+ super()
412
+ @opcode = 6
413
+ end
414
+
415
+ def encode
416
+ datalist = [@opcode]
417
+ format = 'n'
418
+ options.each do |key, val|
419
+ format += "a#{key.to_s.length}x"
420
+ format += "a#{val.to_s.length}x"
421
+ datalist.push key
422
+ datalist.push val
423
+ end
424
+ @buffer = datalist.pack(format)
425
+ return self
426
+ end
427
+
428
+ def decode
429
+ opcode = @buffer[0..1].unpack('n')[0]
430
+ unless opcode == @opcode
431
+ raise ArgumentError, "opcode #{opcode} is not an OACK"
432
+ end
433
+
434
+ @options = decode_options(@buffer[2..-1])
435
+ return self
436
+ end
437
+ end
438
+
439
+ class TftpPacketFactory
440
+ def initialize
441
+ end
442
+
443
+ def create(opcode)
444
+ return case opcode
445
+ when 1 then TftpPacketRRQ
446
+ when 2 then TftpPacketWRQ
447
+ when 3 then TftpPacketDAT
448
+ when 4 then TftpPacketACK
449
+ when 5 then TftpPacketERR
450
+ when 6 then TftpPacketOACK
451
+ else raise ArgumentError, "Unsupported opcode: #{opcode}"
452
+ end.new
453
+ end
454
+
455
+ def parse(buffer)
456
+ unless buffer
457
+ raise ArgumentError, "buffer cannot be empty"
458
+ end
459
+ opcode = buffer[0..1].unpack('n')[0]
460
+ packet = create(opcode)
461
+ packet.buffer = buffer
462
+ packet.decode
463
+ return packet
464
+ end
465
+ end
466
+
467
+ class TftpSession
468
+ attr_accessor :options, :state
469
+ attr_reader :dups, :errors
470
+
471
+ def initialize
472
+ # Agreed upon session options
473
+ @options = {}
474
+ # State of the session, can be one of
475
+ # nil - No state yet
476
+ # :rrq - Just sent rrq, waiting for response
477
+ # :wrq - Just sent wrq, waiting for response
478
+ # :dat - transferring data
479
+ # :oack - Received oack, negotiating options
480
+ # :ack - Acknowledged oack, waiting for response
481
+ # :err - Fatal problems, giving up
482
+ # :done - Session is over, file transferred
483
+ @state = nil
484
+ @dups = 0
485
+ @errors = 0
486
+ @blksize = DefBlkSize
487
+ end
488
+ end
489
+
490
+ class TftpServer < TftpSession
491
+ def initialize
492
+ super()
493
+ @iface = nil
494
+ @port = nil
495
+ @root = nil
496
+ @sessions = []
497
+ end
498
+
499
+ # This method starts a server listening on a given port, to serve up files
500
+ # at a given path. It takes an optional ip to bind to, which defaults to
501
+ # localhost (127.0.0.1).
502
+ def listen(port, path, iface="127.0.0.1")
503
+ @iface = iface
504
+ @port = port
505
+ @root = path
506
+ sock = UDPSocket.new
507
+ sock.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEADDR, true)
508
+ #sock.setsockopt(Socket::SOL_SOCKET, Socket::SO_RCVTIMEO, SockTimeout)
509
+ sock.bind(iface, port)
510
+ $tftplog.info('tftp+') { "Bound to #{iface} on port #{port}" }
511
+
512
+ factory = TftpPacketFactory.new
513
+ retry_count = 0
514
+ loop do
515
+ $tftplog.debug('tftp+') { "Waiting for incoming datagram..." }
516
+ msg = sender = nil
517
+ begin
518
+ status = Timeout::timeout(SockTimeout) {
519
+ msg, sender = sock.recvfrom(MaxBlkSize)
520
+ }
521
+ rescue Timeout::Error => details
522
+ retry_count += 1
523
+ if retry_count > MaxRetry
524
+ msg = "Timeout! Max retries exceeded. Giving up."
525
+ $tftplog.error('tftp+') { msg }
526
+ raise TftpError, msg
527
+ else
528
+ $tftplog.warn('tftp+') { "Timeout! Lets try again." }
529
+ next
530
+ end
531
+ end
532
+ prot, rport, rhost, rip = sender
533
+
534
+ pkt = factory.parse(msg)
535
+ $tftplog.debug('tftp+') { "pkt is #{pkt}" }
536
+
537
+ key = "#{rip}-#{rport}"
538
+ handler = nil
539
+ unless @sessions.has_key? key
540
+ handler = TftpServerHandler.new(rhost, rport, key, root)
541
+ else
542
+ handler = @sessions[key]
543
+ end
544
+ handler.handle(pkt)
545
+ end
546
+ end
547
+ end
548
+
549
+ class TftpServerHandler < TftpSession
550
+ def initialize(rhost, rport, key, root)
551
+ super()
552
+ @host = rhost
553
+ @port = rport
554
+ @key = key
555
+ @root = root
556
+ end
557
+
558
+ def handle(pkt)
559
+ end
560
+ end
561
+
562
+ class TftpClient < TftpSession
563
+ attr_reader :host, :port
564
+
565
+ def initialize(host, port)
566
+ super()
567
+ @host = host
568
+ # Force the port to a string type.
569
+ @iport = port.to_s
570
+ @port = nil
571
+ # FIXME - move host and port args to download method
572
+
573
+ begin
574
+ @address = Resolv::IPv4.create(@host)
575
+ rescue ArgumentError => details
576
+ # So, @host doesn't look like an IP. Resolve it.
577
+ # A Resolv::ResolvError exception could be raised here, let it
578
+ # filter up.
579
+ @address = Resolv::DNS.new.getaddress(@host)
580
+ end
581
+ end
582
+
583
+ # FIXME - this method is too big
584
+ def download(filename, output, options={})
585
+ @blksize = options[:blksize] if options.has_key? :blksize
586
+ $tftplog.debug('tftp+') { "Opening output file #{output}" }
587
+ fout = File.open(output, "w")
588
+ sock = UDPSocket.new
589
+
590
+ pkt = TftpPacketRRQ.new
591
+ pkt.filename = filename
592
+ pkt.mode = 'octet' # FIXME - shouldn't hardcode this
593
+ pkt.options = options
594
+ $tftplog.info('tftp+') { "Sending download request for #{filename}" }
595
+ $tftplog.info('tftp+') { "host = #{@host}, port = #{@iport}" }
596
+ sock.send(pkt.encode.buffer, 0, @host, @iport)
597
+ @state = :rrq
598
+
599
+ factory = TftpPacketFactory.new
600
+
601
+ blocknumber = 1
602
+ retry_count = 0
603
+ loop do
604
+ $tftplog.debug('tftp+') { "Waiting for incoming datagram..." }
605
+ msg = sender = nil
606
+ begin
607
+ status = Timeout::timeout(SockTimeout) {
608
+ msg, sender = sock.recvfrom(MaxBlkSize)
609
+ }
610
+ rescue Timeout::Error => details
611
+ retry_count += 1
612
+ if retry_count > MaxRetry
613
+ msg = "Timeout! Max retries exceeded. Giving up."
614
+ $tftplog.error('tftp+') { msg }
615
+ raise TftpError, msg
616
+ else
617
+ $tftplog.debug('tftp+') { "Timeout! Lets try again." }
618
+ next
619
+ end
620
+ end
621
+ prot, rport, rhost, rip = sender
622
+ $tftplog.info('tftp+') { "Received #{msg.length} byte packet" }
623
+ $tftplog.debug('tftp+') { "Remote port is #{rport} and remote host is #{rhost}" }
624
+
625
+ if @address.to_s != rip
626
+ # Skip it
627
+ @errors += 1
628
+ $stderr.write "It is a rogue packet! #{sender[1]} #{sender[2]}\n"
629
+ next
630
+ elsif @port and @port != rport.to_s
631
+ # Skip it
632
+ @errors += 1
633
+ $stderr.write "It is a rogue packet! #{sender[1]} #{sender[2]}\n"
634
+ next
635
+ else not @port
636
+ # Set this as our TID
637
+ $tftplog.info('tftp+') { "Set remote TID to #{@port}" }
638
+ @port = rport.to_s
639
+ end
640
+
641
+ pkt = factory.parse(msg)
642
+ $tftplog.debug('tftp+') { "pkt is #{pkt}" }
643
+
644
+ # FIXME - Refactor this into separate methods to handle each case.
645
+ if pkt.is_a? TftpPacketRRQ
646
+ # Skip it, but info('tftp+')rm the sender.
647
+ err = TftpPacketERR.new
648
+ err.errorcode = 4 # illegal op
649
+ sock.send(err.encode.buffer, 0, @host, @port)
650
+ @errors += 1
651
+ $stderr.write "It is a RRQ packet in download, state #{@state}\n"
652
+
653
+ elsif pkt.is_a? TftpPacketWRQ
654
+ # Skip it, but info('tftp+')rm the sender.
655
+ err = TftpPacketERR.new
656
+ err.errorcode = 4 # illegal op
657
+ sock.send(err.encode.buffer, 0, @host, @port)
658
+ @errors += 1
659
+ $stderr.write "It is a WRQ packet in download, state #{@state}\n"
660
+
661
+ elsif pkt.is_a? TftpPacketACK
662
+ # Skip it, but info('tftp+')rm the sender.
663
+ err = TftpPacketERR.new
664
+ err.errorcode = 4 # illegal op
665
+ sock.send(err.encode.buffer, 0, @host, @port)
666
+ @errors += 1
667
+ $stderr.write "It is a ACK packet in download, state #{@state}\n"
668
+
669
+ elsif pkt.is_a? TftpPacketERR
670
+ @errors += 1
671
+ raise TftpError, "ERR packet: #{pkt.errmsg}"
672
+
673
+ elsif pkt.is_a? TftpPacketOACK
674
+ unless @state == :rrq
675
+ @errors += 1
676
+ $stderr.write "It is a OACK in state #{@state}"
677
+ next
678
+ end
679
+
680
+ @state = :oack
681
+ # Are the acknowledged options the same as ours?
682
+ # FIXME - factor this into the OACK class?
683
+ if pkt.options
684
+ pkt.options do |optname, optval|
685
+ case optname
686
+ when :blksize
687
+ # The blocksize can be <= what we proposed.
688
+ unless options.has_key? :blksize
689
+ # Hey, we didn't ask for a blocksize option...
690
+ err = TftpPacketERR.new
691
+ err.errorcode = 8 # failed negotiation
692
+ sock.send(err.encode.buffer, 0, @host, @port)
693
+ raise TftpError, "It is a OACK with blocksize when we didn't ask for one."
694
+ end
695
+
696
+ if optval <= options[:blksize] and optval >= MinBlkSize
697
+ # Valid. Lets use it.
698
+ options[:blksize] = optval
699
+ end
700
+ else
701
+ # FIXME - refactor err packet handling from above...
702
+ # Nothing that we don't know of should be in the
703
+ # oack packet.
704
+ err = TftpPacketERR.new
705
+ err.errorcode = 8 # failed negotiation
706
+ sock.send(err.encode.buffer, 0, @host, @port)
707
+ raise TftpError, "Failed to negotiate options: #{pkt.options}"
708
+ end
709
+ end
710
+ # SUCCESSFUL NEGOTIATION
711
+ # If we're here, then we're happy with the options in the
712
+ # OACK. Send an ACK of block 0 to ACK the OACK.
713
+ # FIXME - further negotiation required here?
714
+ ack = TftpPacketACK.new
715
+ ack.blocknumber = 0
716
+ sock.send(ack.encode.buffer, 0, @host, @port)
717
+ @state = :ack
718
+ else
719
+ # OACK with no options?
720
+ err = TftpPacketERR.new
721
+ err.errorcode = 8 # failed negotiation
722
+ sock.send(err.encode.buffer, 0, @host, @port)
723
+ raise TftpError, "OACK with no options"
724
+ end
725
+
726
+ # Done parsing. If we didn't raise an exception, then we need
727
+ # to send an ACK to the server, with block number 0.
728
+ ack = TftpPacketACK.new
729
+ ack.blocknumber = 0
730
+ $tftplog.info('tftp+') { "Sending ACK to OACK" }
731
+ sock.send(ack.encode.buffer, 0, @host, @port)
732
+ @state = :ack
733
+
734
+ elsif pkt.is_a? TftpPacketDAT
735
+ # If the state is :rrq, and we sent options, then the
736
+ # server didn't send us an oack, and the options were refused.
737
+ # FIXME - we need to handle all possible options and set them
738
+ # back to their defaults here, not just blocksize.
739
+ if @state == :rrq and options.has_key? :blksize
740
+ @blksize = DefBlkSize
741
+ end
742
+
743
+ @state = :dat
744
+ $tftplog.info('tftp+') { "It is a DAT packet, block #{pkt.blocknumber}" }
745
+ $tftplog.debug('tftp+') { "DAT size is #{pkt.data.length}" }
746
+
747
+ ack = TftpPacketACK.new
748
+ ack.blocknumber = pkt.blocknumber
749
+
750
+ $tftplog.info('tftp+') { "Sending ACK to block #{ack.blocknumber}" }
751
+ sock.send(ack.encode.buffer, 0, @host, @port)
752
+
753
+ # Check for dups
754
+ if pkt.blocknumber <= blocknumber
755
+ $tftplog.warn('tftp+') { "It is a DUP for block #{blocknumber}" }
756
+ @dups += 1
757
+ elsif pkt.blocknumber = blocknumber+1
758
+ $tftplog.debug('tftp+') { "It is a properly ordered DAT packet" }
759
+ blocknumber += 1
760
+ else
761
+ # Skip it, but info('tftp+')rm the sender.
762
+ err = TftpPacketERR.new
763
+ err.errorcode = 4 # illegal op
764
+ sock.send(err.encode.buffer, 0, @host, @port)
765
+ @errors += 1
766
+ $stderr.write "It is a future packet!\n"
767
+ end
768
+
769
+ # Call any block passed.
770
+ if block_given?
771
+ yield pkt
772
+ end
773
+
774
+ # Write the data to the file.
775
+ fout.print pkt.data
776
+ # If the size is less than our blocksize, we're done.
777
+ $tftplog.debug('tftp+') { "pkt.data.length is #{pkt.data.length}" }
778
+ if pkt.data.length < @blksize
779
+ $tftplog.info('tftp+') { "It is a last packet." }
780
+ fout.close
781
+ @state = :done
782
+ break
783
+ end
784
+ else
785
+ msg = "It is an unknown packet: #{pkt}"
786
+ $tftplog.error('tftp+') { msg }
787
+ raise TftpError, msg
788
+ end
789
+ end
790
+ end
791
+ end
792
+
793
+ # If invoked directly...
794
+ if __FILE__ == $0
795
+ # Simple client maybe?
796
+ end
data/test/test.rb ADDED
@@ -0,0 +1,58 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $:.unshift File.join(File.dirname(__FILE__), "..", "lib")
4
+ require 'net/tftp+'
5
+ require 'test/unit'
6
+
7
+ class TestTftp < Test::Unit::TestCase
8
+ def test_simple
9
+ rrq = TftpPacketRRQ.new
10
+ rrq.filename = 'myfilename'
11
+ rrq.mode = 'octet'
12
+ rrq.encode
13
+ assert_equal("\000\001myfilename\000octet\000", rrq.buffer)
14
+ assert_equal(1, rrq.opcode)
15
+ rrq.decode
16
+ assert_equal('myfilename', rrq.filename)
17
+ assert_equal('octet', rrq.mode)
18
+
19
+ wrq = TftpPacketWRQ.new
20
+ wrq.buffer = "\000\002myfilename\000octet\000"
21
+ wrq.decode
22
+ assert_equal('myfilename', wrq.filename)
23
+ assert_equal('octet', wrq.mode)
24
+ assert_equal(2, wrq.opcode)
25
+ wrq.encode.decode
26
+ assert_equal('myfilename', wrq.filename)
27
+ assert_equal('octet', wrq.mode)
28
+ assert_equal(2, wrq.opcode)
29
+
30
+ dat = TftpPacketDAT.new
31
+ sampledat = "\000\001\002\003\004\005"
32
+ dat.data = sampledat
33
+ dat.encode.decode
34
+ assert_equal(sampledat, dat.data)
35
+ assert_equal(6, dat.data.length)
36
+ assert_equal(3, dat.opcode)
37
+
38
+ ack = TftpPacketACK.new
39
+ ack.blocknumber = 5
40
+ assert_equal(4, ack.opcode)
41
+ assert_equal(5, ack.encode.decode.blocknumber)
42
+
43
+ err = TftpPacketERR.new
44
+ err.errorcode = 3
45
+ assert_equal('Disk full or allocation exceeded.',
46
+ err.encode.decode.errmsg)
47
+ assert_equal(5, err.opcode)
48
+
49
+ oack = TftpPacketOACK.new
50
+ oack_options = {
51
+ :blksize => 4096
52
+ }
53
+ oack.options = oack_options
54
+ oack.encode.decode
55
+ assert_equal('4096', oack.options[:blksize])
56
+ assert_equal(6, oack.opcode)
57
+ end
58
+ end
metadata ADDED
@@ -0,0 +1,49 @@
1
+ --- !ruby/object:Gem::Specification
2
+ rubygems_version: 0.9.0
3
+ specification_version: 1
4
+ name: tftpplus
5
+ version: !ruby/object:Gem::Version
6
+ version: "0.2"
7
+ date: 2006-12-08 00:00:00 -05:00
8
+ summary: A pure tftp implementation with support for variable block sizes
9
+ require_paths:
10
+ - lib
11
+ email: msoulier@digitaltorque.ca
12
+ homepage: http://tftpplus.rubyforge.org
13
+ rubyforge_project:
14
+ description: A new tftp library for clients and servers that supports RFCs 1350, 2347 and 2348 (ie. variable block sizes). It includes a sample client implementation, and will eventually include a multi-threaded server as well.
15
+ autorequire:
16
+ default_executable:
17
+ bindir: bin
18
+ has_rdoc: false
19
+ required_ruby_version: !ruby/object:Gem::Version::Requirement
20
+ requirements:
21
+ - - ">"
22
+ - !ruby/object:Gem::Version
23
+ version: 0.0.0
24
+ version:
25
+ platform: ruby
26
+ signing_key:
27
+ cert_chain:
28
+ post_install_message:
29
+ authors:
30
+ - Michael P. Soulier
31
+ files:
32
+ - lib/net/tftp+.rb
33
+ - README
34
+ - ChangeLog
35
+ - test/test.rb
36
+ test_files:
37
+ - test/test.rb
38
+ rdoc_options: []
39
+
40
+ extra_rdoc_files: []
41
+
42
+ executables:
43
+ - tftp_client.rb
44
+ extensions: []
45
+
46
+ requirements: []
47
+
48
+ dependencies: []
49
+