tftpplus 0.2

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