tftpplus 0.2 → 0.3

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 +135 -19
  2. data/README +6 -2
  3. data/bin/tftp_client.rb +20 -9
  4. data/lib/net/tftp+.rb +577 -108
  5. data/test/test.rb +71 -6
  6. metadata +3 -3
data/ChangeLog CHANGED
@@ -1,47 +1,163 @@
1
- 2006-12-08 msoulier
2
- * Fixed handling of remote TID.
3
- * Added tar task to Rakefile.
1
+ 2007-03-15 00:00 msoulier
4
2
 
5
- 2006-10-10 msoulier
6
- * Added site, and Rakefile entry to push site.
3
+ * ChangeLog: Updated ChangeLog
4
+
5
+ 2007-03-14 01:32 msoulier
6
+
7
+ * lib/net/tftp+.rb, test/test.rb: Added testcases.
8
+
9
+ 2007-03-13 16:27 msoulier
10
+
11
+ * test/test.rb: Updated some testcases.
12
+
13
+ 2007-03-10 23:42 msoulier
14
+
15
+ * bin/tftp_server.rb, lib/net/tftp+.rb: First working downloads
16
+ from server. Woohoo!
17
+
18
+ 2007-03-09 05:04 msoulier
19
+
20
+ * lib/net/tftp+.rb: Fixed lots of silly little runtime errors in
21
+ testing.
22
+ Still not transferring properly, but we're much closer.
23
+
24
+ 2007-03-07 03:22 msoulier
25
+
26
+ * lib/net/tftp+.rb: Filled-out the timeout implementation. Server
27
+ still fails to start a download
28
+ at this point.
29
+
30
+ 2007-03-06 10:16 msoulier
31
+
32
+ * lib/net/tftp+.rb: Added check of file path in download request.
33
+
34
+ 2006-12-22 23:43 msoulier
35
+
36
+ * lib/net/tftp+.rb: Redefined TftpErrorType
37
+
38
+ 2006-12-22 04:41 msoulier
39
+
40
+ * bin/tftp_server.rb, lib/net/tftp+.rb: Starting sample server
41
+ implementation.
42
+
43
+ 2006-12-22 04:27 msoulier
44
+
45
+ * bin/tftp_client.rb, lib/net/tftp+.rb: First pass at adding server
46
+ implementation.
47
+
48
+ 2006-12-21 05:39 msoulier
49
+
50
+ * Rakefile, lib/net/tftp+.rb: Added a little more server. Cleaned
51
+ up client.
52
+
53
+ 2006-12-19 04:35 msoulier
54
+
55
+ * lib/net/tftp+.rb: Fleshing out some of the server and handler
56
+ code.
57
+
58
+ 2006-12-19 01:07 msoulier
59
+
60
+ * bin/tftp_client.rb, lib/net/tftp+.rb: Cleaned up the client a
61
+ bit.
62
+
63
+ 2006-12-09 03:38 msoulier
64
+
65
+ * ChangeLog: Updated ChangeLog
66
+
67
+ 2006-12-09 03:34 msoulier
68
+
69
+ * Rakefile, doc, doc/rfc1350.txt, doc/rfc2347.txt, doc/rfc2348.txt:
70
+ Added tar task to rakefile
71
+
72
+ 2006-12-09 03:23 msoulier
73
+
74
+ * ChangeLog, README: Updated ChangeLog and README
75
+
76
+ 2006-12-09 03:18 msoulier
77
+
78
+ * Rakefile: Updated version
79
+
80
+ 2006-12-09 03:17 msoulier
81
+
82
+ * lib/net/tftp+.rb: Fixing remote TID RFC compliance.
83
+
84
+ 2006-12-08 23:41 msoulier
85
+
86
+ * lib/net/tftp+.rb: Updated log messages
87
+
88
+ 2006-10-17 02:33 msoulier
89
+
90
+ * lib/net/tftp+.rb: Fix calls to tftpassert.
91
+
92
+ 2006-10-16 00:48 msoulier
93
+
94
+ * lib/net/tftp+.rb: Added some rdoc
95
+
96
+ 2006-10-15 23:58 msoulier
97
+
98
+ * bin/tftp_client.rb, lib/net/tftp+.rb: Moved the logger to the
99
+ client, and put in a nil logger in the library.
100
+
101
+ 2006-10-15 20:53 msoulier
102
+
103
+ * lib/net/tftp+.rb, test/test.rb: Added a stdlib logger
104
+
105
+ 2006-10-10 02:52 msoulier
106
+
107
+ * ChangeLog, Rakefile, site, site/index.html: Added site directory
108
+ with Rakefile entry to push it public
109
+
110
+ 2006-10-10 02:10 msoulier
111
+
112
+ * Rakefile: Refactored Rakefile
113
+
114
+ 2006-10-10 02:08 msoulier
115
+
116
+ * ChangeLog, README, Rakefile: Added Rakefile for project
117
+ management.
7
118
 
8
119
  2006-10-10 01:46 msoulier
9
120
 
10
- * trunk/ChangeLog, trunk/README, trunk/doc,
11
- trunk/lib/net/tftp+.rb: Adding Rakefile
121
+ * ChangeLog, README, doc, lib/net/tftp+.rb: Adding Rakefile
12
122
 
13
123
  2006-10-06 01:35 msoulier
14
124
 
15
- * trunk/lib/net/tftp+.rb: Optimizing Tftp object dispatch
125
+ * lib/net/tftp+.rb: Optimizing Tftp object dispatch
16
126
 
17
127
  2006-10-04 01:27 msoulier
18
128
 
19
- * trunk/lib/net/tftp+.rb: minor method rename
129
+ * lib/net/tftp+.rb: minor method rename
20
130
 
21
131
  2006-09-25 02:11 msoulier
22
132
 
23
- * trunk/lib/net/tftp+.rb: Added timeouts on recvfrom
133
+ * lib/net/tftp+.rb: Added timeouts on recvfrom
24
134
 
25
135
  2006-09-24 02:39 msoulier
26
136
 
27
- * trunk/lib/net/tftp+.rb: Updated debugging output
137
+ * lib/net/tftp+.rb: Updated debugging output
138
+
139
+ 2006-09-24 02:34 msoulier
140
+
141
+ * bin/tftp_client.rb: Making client executable
142
+
143
+ 2006-09-24 02:32 msoulier
144
+
145
+ * bin, share: Renaming share to bin
28
146
 
29
147
  2006-09-24 02:31 msoulier
30
148
 
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.
149
+ * lib/net/tftp+.rb, share, share/tftp_client.rb, test/test.rb:
150
+ Moved client code into its own sample client.
33
151
 
34
152
  2006-09-22 16:48 msoulier
35
153
 
36
- * trunk/lib/net/tftp+.rb: Pulling hardcoded test
154
+ * lib/net/tftp+.rb: Pulling hardcoded test
37
155
 
38
156
  2006-09-22 16:44 msoulier
39
157
 
40
- * doc, lib, test, trunk/doc, trunk/lib, trunk/test: Setting up
41
- branch structure.
158
+ * doc, lib, test, doc, lib, test: Setting up branch structure.
42
159
 
43
- 2006-09-22 16:42 msoulier
160
+ 2006-09-22 16:44 msoulier
44
161
 
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.
162
+ * branches, tags, .: Setting up branch structure.
47
163
 
data/README CHANGED
@@ -7,13 +7,17 @@ A new tftp library for clients and servers that supports RFCs 1350, 2347 and
7
7
  Release Notes:
8
8
  ==============
9
9
 
10
+ About version 0.3:
11
+ ------------------
12
+ - First working server class.
13
+
10
14
  About version 0.2:
11
- ==================
15
+ ------------------
12
16
 
13
17
  - Fixed handling of remote TID.
14
18
 
15
19
  About version 0.1:
16
- ==================
20
+ ------------------
17
21
 
18
22
  - Added timeouts on recvfrom
19
23
  - Updated debugging output
data/bin/tftp_client.rb CHANGED
@@ -47,9 +47,9 @@ EOF
47
47
  $log.level = Logger::DEBUG
48
48
  $log.debug('client') { "Debug output requested" }
49
49
  end
50
- opts.on('-b', '--blksize=', 'Blocksize option: 8-65536 bytes') do |b|
50
+ opts.on('-b', '--blocksize=', 'Blocksize option: 8-65536 bytes') do |b|
51
51
  options.blksize = b.to_i
52
- $log.debug('client') { "blksize is #{b}" }
52
+ $log.debug('client') { "blocksize is #{b}" }
53
53
  end
54
54
  opts.on_tail('-h', '--help', 'Show this message') do
55
55
  puts opts
@@ -67,7 +67,7 @@ EOF
67
67
  #end
68
68
  unless options.blksize >= 8 and options.blksize <= 65536
69
69
  raise OptionParser::InvalidOption,
70
- "blksize can only be between 8 and 65536 bytes"
70
+ "blocksize can only be between 8 and 65536 bytes"
71
71
  end
72
72
  unless options.port > 0 and options.port < 65537
73
73
  raise OptionParser::InvalidOption,
@@ -75,7 +75,6 @@ EOF
75
75
  end
76
76
  rescue Exception => details
77
77
  $stderr.puts details.to_s
78
- $stderr.puts opts
79
78
  exit 1
80
79
  end
81
80
 
@@ -88,13 +87,25 @@ def main
88
87
  size = 0
89
88
  start = Time.now
90
89
  $log.info('client') { "Starting download of #{options.filename} from #{options.host}" }
91
- $log.info('client') { "Options: blksize = #{options.blksize}" }
90
+ $log.info('client') { "Options: blocksize = #{options.blksize}" }
92
91
 
93
- client = TftpClient.new(options.host, options.port)
92
+ begin
93
+ client = TftpClient.new(options.host, options.port)
94
+ rescue SocketError => details
95
+ $stderr.puts "Error looking up host #{options.host}"
96
+ exit 1
97
+ end
98
+
94
99
  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" }
100
+
101
+ begin
102
+ client.download(options.filename, options.filename, tftp_opts) do |pkt|
103
+ size += pkt.data.length
104
+ $log.debug('client') { "Downloaded #{size} bytes" }
105
+ end
106
+ rescue TftpError => details
107
+ $stderr.puts "Fatal exception in transfer"
108
+ $stderr.puts details
98
109
  end
99
110
 
100
111
  finish = Time.now
data/lib/net/tftp+.rb CHANGED
@@ -20,9 +20,13 @@ MinBlkSize = 8
20
20
  DefBlkSize = 512
21
21
  MaxBlkSize = 65536
22
22
  SockTimeout = 5
23
+ SendTimeout = 5
23
24
  MaxDups = 20
24
25
  Assertions = true
25
26
  MaxRetry = 5
27
+ MaxBlockNum = 65535
28
+ DefRoot = '/tftpboot'
29
+ MaxBindAttempts = 5
26
30
 
27
31
  # This class is a Nil logging device. It catches all of the logger calls and
28
32
  # does nothing with them. It is up to the client to provide a real logger
@@ -38,9 +42,53 @@ end
38
42
  # one.
39
43
  $tftplog = TftpNilLogger.new
40
44
 
45
+ # Convenience function for debug logging.
46
+ def debug(msg)
47
+ $tftplog.debug('tftp+') { msg }
48
+ end
49
+
50
+ # Convenience function for info logging.
51
+ def info(msg)
52
+ $tftplog.info('tftp+') { msg }
53
+ end
54
+
55
+ # Convenience function for warn logging.
56
+ def warn(msg)
57
+ $tftplog.warn('tftp+') { msg }
58
+ end
59
+
60
+ # Convenience function for error logging.
61
+ def error(msg)
62
+ $tftplog.error('tftp+') { msg }
63
+ end
64
+
65
+ # This class is a convenience for defining the common tftp error codes, and
66
+ # making them more readable in the code.
67
+ class TftpErrorType
68
+ @notDefined = 0
69
+ @fileNotFound = 1
70
+ @accessViolation = 2
71
+ @diskFull = 3
72
+ @illegalTftpOp = 4
73
+ @unknownTID = 5
74
+ @fileAlreadyExists = 6
75
+ @noSuchUser = 7
76
+ @failedNegotiation = 8
77
+ class <<self
78
+ attr_reader :notDefined, :fileNotFound, :accessViolation
79
+ attr_reader :diskFull, :illegalTftpOp, :unknownTID
80
+ attr_reader :fileAlreadyExists, :noSuchUser, :failedNegotiation
81
+ end
82
+ end
83
+
84
+ # This exception is used to signal errors.
41
85
  class TftpError < RuntimeError
42
86
  end
43
87
 
88
+ # This exception is used to signal success to the server instance.
89
+ class TftpSuccess < Exception
90
+ end
91
+
44
92
  # This function is a custom assertion in the library to catch unsupported
45
93
  # states and types. If the assertion fails, msg is raised in a TftpError
46
94
  # exception.
@@ -80,8 +128,8 @@ class TftpPacket
80
128
  def options=(opts)
81
129
  myopts = {}
82
130
  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}" }
131
+ debug "looping on key #{key}, val #{val}"
132
+ debug "class of key is #{key.class}"
85
133
  tftpassert("options keys must be symbols") { key.class == Symbol }
86
134
  myopts[key.to_s] = val.to_s
87
135
  end
@@ -93,7 +141,7 @@ class TftpPacket
93
141
  return @options
94
142
  end
95
143
 
96
- protected
144
+ private
97
145
 
98
146
  # This method takes the portion of the buffer containing the options and
99
147
  # decodes it, returning a hash of the option name/value pairs, with the
@@ -120,7 +168,7 @@ class TftpPacket
120
168
  name = struct.shift
121
169
  value = struct.shift
122
170
  options[name.to_sym] = value
123
- $tftplog.debug('tftp+') { "decoded option #{name} with value #{value}" }
171
+ debug "decoded option #{name} with value #{value}"
124
172
  end
125
173
  return options
126
174
  end
@@ -194,7 +242,7 @@ class TftpPacketInitial < TftpPacket
194
242
  raise TftpError, "filename is the null string"
195
243
  end
196
244
  @mode = struct[2]
197
- unless valid_mode? @mode
245
+ unless TftpPacketInitial.valid_mode? @mode
198
246
  raise TftpError, "mode #{@mode} is not valid"
199
247
  end
200
248
 
@@ -219,11 +267,12 @@ class TftpPacketInitial < TftpPacket
219
267
  return self
220
268
  end
221
269
 
222
- protected
270
+ private
223
271
 
224
272
  # This method is a boolean validator that returns true if the blocksize
225
273
  # passed is valid, and false otherwise.
226
- def valid_blocksize?(blksize)
274
+ # FIXME - is anyone calling this?
275
+ def self.valid_blocksize?(blksize)
227
276
  blksize = blksize.to_i
228
277
  if blksize >= 8 and blksize <= 65464
229
278
  return true
@@ -235,9 +284,11 @@ class TftpPacketInitial < TftpPacket
235
284
  # This method is a boolean validator that returns true of the mode passed
236
285
  # is valid, and false otherwise. The modes of 'netascii', 'octet' and
237
286
  # 'mail' are valid, even though only 'octet' is currently implemented.
238
- def valid_mode?(mode)
287
+ def self.valid_mode?(mode)
239
288
  case mode
240
- when "netascii", "octet", "mail"
289
+ # FIXME - implement support for netascii. don't care about mail
290
+ #when "netascii", "octet", "mail"
291
+ when "octet"
241
292
  return true
242
293
  else
243
294
  return false
@@ -277,6 +328,9 @@ class TftpPacketDAT < TftpPacket
277
328
  end
278
329
 
279
330
  def encode
331
+ debug "DAT encode: @opcode is #{@opcode}"
332
+ debug "DAT encode: @blocknumber is #{@blocknumber}"
333
+ debug "DAT encode: @data has #{@data.length} bytes in it"
280
334
  unless @opcode and @blocknumber and @data
281
335
  raise ArgumentError, "Required fields missing!"
282
336
  end
@@ -354,6 +408,8 @@ end
354
408
  # 7 No such user.
355
409
  # 8 Failed negotiation
356
410
  class TftpPacketERR < TftpPacket
411
+ # FIXME - Reevaluate the ErrMsg, and whether we encourage senderror to
412
+ # expect one.
357
413
  attr_reader :extended_errmsg
358
414
  attr_accessor :errorcode, :errmsg, :buffer
359
415
  ErrMsgs = [
@@ -453,14 +509,18 @@ class TftpPacketFactory
453
509
  end
454
510
 
455
511
  def parse(buffer)
456
- unless buffer
512
+ unless buffer and buffer.class == String
457
513
  raise ArgumentError, "buffer cannot be empty"
458
514
  end
459
- opcode = buffer[0..1].unpack('n')[0]
460
- packet = create(opcode)
461
- packet.buffer = buffer
462
- packet.decode
463
- return packet
515
+ begin
516
+ opcode = buffer[0..1].unpack('n')[0]
517
+ packet = create(opcode)
518
+ packet.buffer = buffer
519
+ packet.decode
520
+ return packet
521
+ rescue
522
+ raise TftpError, "Parsing errors, packet looks bad."
523
+ end
464
524
  end
465
525
  end
466
526
 
@@ -471,6 +531,7 @@ class TftpSession
471
531
  def initialize
472
532
  # Agreed upon session options
473
533
  @options = {}
534
+ # FIXME - should we make the state an object of its own?
474
535
  # State of the session, can be one of
475
536
  # nil - No state yet
476
537
  # :rrq - Just sent rrq, waiting for response
@@ -483,79 +544,472 @@ class TftpSession
483
544
  @state = nil
484
545
  @dups = 0
485
546
  @errors = 0
486
- @blksize = DefBlkSize
547
+ @options = { :blksize => DefBlkSize }
548
+ end
549
+
550
+ # This method is used to send an error packet to a given address and port,
551
+ # with an given error code.
552
+ def senderror(sock, errorcode, address, port)
553
+ @errors += 1
554
+ err = TftpPacketERR.new
555
+ err.errorcode = errorcode
556
+ sock.send(err.encode.buffer, 0, address, port)
487
557
  end
488
558
  end
489
559
 
560
+ # This server instance currently listens on a single specified port to
561
+ # initiate a session. From there it waits in a select() loop on all sockets
562
+ # for itself and all handlers, dispatching ready sockets to their handlers and
563
+ # instantiating new handlers as needed.
490
564
  class TftpServer < TftpSession
491
- def initialize
565
+ def initialize(root)
492
566
  super()
567
+ @root = root
493
568
  @iface = nil
494
569
  @port = nil
495
- @root = nil
496
- @sessions = []
570
+ @handlers = {}
497
571
  end
498
572
 
499
573
  # This method starts a server listening on a given port, to serve up files
500
574
  # 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")
575
+ # INADDR_ANY.
576
+ def listen(port, path, iface="")
503
577
  @iface = iface
504
578
  @port = port
505
579
  @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}" }
580
+ main_sock = UDPSocket.new
581
+ main_sock.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEADDR, true)
582
+ main_sock.bind(iface, port)
583
+ info "Bound to #{@iface} on port #{@port}"
511
584
 
512
585
  factory = TftpPacketFactory.new
513
586
  retry_count = 0
514
587
  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
588
+ allsockets = []
589
+ allsockets << main_sock
590
+ @handlers.each do |key, val|
591
+ allsockets << val.sock
592
+ end
593
+ debug "Performing select on all sockets"
594
+ debug "allsockets array is #{allsockets}"
595
+ selectarr = select(allsockets, nil, nil, SockTimeout)
596
+ readysocks = selectarr ? selectarr[0] : []
597
+
598
+ deletion_list = []
599
+
600
+ # FIXME - this loop is really large...
601
+ readysocks.each do |readysock|
602
+ if readysock.equal?(main_sock)
603
+ debug "Traffic on the main socket"
604
+ msg, sender = readysock.recvfrom(MaxBlkSize)
605
+ prot, rport, rhost, rip = sender
606
+
607
+ pkt = factory.parse(msg)
608
+ debug "pkt is a #{pkt}"
609
+
610
+ if pkt.is_a? TftpPacketRRQ
611
+ debug "Handling packet as an RRQ"
612
+
613
+ key = "#{rip}:#{rport}"
614
+ handler = nil
615
+ unless @handlers.has_key?(key)
616
+ handler = TftpServerHandler.new(rhost,
617
+ rport,
618
+ key,
619
+ @root,
620
+ @iface,
621
+ factory,
622
+ :rrq)
623
+ @handlers[key] = handler
624
+ begin
625
+ handler.handle(pkt)
626
+ rescue TftpError => details
627
+ error "Fatal exception thrown from handler at creation #{key}: #{details}"
628
+ deletion_list.push(key)
629
+ end
630
+ # Note: A TftpSuccess exception isn't possible
631
+ # here.
632
+ else
633
+ senderror(main_sock,
634
+ TftpErrorType.unknownTID,
635
+ rhost,
636
+ rport)
637
+ error "Received RRQ for session #{key}, which already exists"
638
+ @errors += 1
639
+ next
640
+ end
641
+ elsif pkt.is_a? TftpPacketWRQ
642
+ debug "Handling packet as a WRQ"
643
+
644
+ senderror(main_sock,
645
+ TftpErrorType.illegalTftpOp,
646
+ rhost,
647
+ rport)
648
+ warn "Support for uploads not yet implemented."
649
+ next
650
+ else
651
+ debug "Handling packet as a non-RRQ/WRQ"
652
+ # FIXME - this will prevent symmetric udp from working
653
+ # if I ever care to implement it.
654
+ senderror(main_sock,
655
+ TftpErrorType.illegalTftpOp,
656
+ rhost,
657
+ rport)
658
+ error "Only RRQ and WRQ operations are valid on the main socket."
659
+ @errors += 1
660
+ next
661
+ end
527
662
  else
528
- $tftplog.warn('tftp+') { "Timeout! Lets try again." }
529
- next
663
+
664
+ debug "Not the main socket. Hunting for the right socket to match socket #{readysock}"
665
+ found = false
666
+ @handlers.each do |key, handler|
667
+ if readysock.equal?(handler.sock)
668
+ debug "Found it. Handler is #{handler.key}"
669
+ found = true
670
+ begin
671
+ handler.handle
672
+ rescue TftpSuccess => details
673
+ info "Successful transfer for handler #{key}: #{details}"
674
+ deletion_list.push(key)
675
+ rescue Exception => details
676
+ error "Fatal exception thrown from handler #{key}: #{details}"
677
+ deletion_list.push(key)
678
+ ensure
679
+ debug "Breaking out of handler iteration"
680
+ break
681
+ end
682
+ end
683
+ end
684
+
685
+ debug "About to process deletion list"
686
+ deletion_list.each do |key|
687
+ debug "Deleting handler #{key}"
688
+ @handlers.delete(key)
689
+ end
690
+
691
+ unless found
692
+ # FIXME - should I do more here?
693
+ error "Hey, I didn't find the handler for this packet!"
694
+ @errors += 1
695
+ end
530
696
  end
531
697
  end
532
- prot, rport, rhost, rip = sender
533
-
534
- pkt = factory.parse(msg)
535
- $tftplog.debug('tftp+') { "pkt is #{pkt}" }
536
698
 
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]
699
+ # Loop on each handler and see if they've timed-out.
700
+ now = Time.now
701
+ @handlers.each do |key, handler|
702
+ if now - handler.timesent > SendTimeout
703
+ info "Handler #{key} has timed-out"
704
+ handler.timeout()
705
+ end
543
706
  end
544
- handler.handle(pkt)
545
707
  end
546
708
  end
547
709
  end
548
710
 
711
+ # The server handler class is responsible for handling a single tftp session.
712
+ # One of these will be instantiated per client.
549
713
  class TftpServerHandler < TftpSession
550
- def initialize(rhost, rport, key, root)
714
+ attr_reader :timesent, :sock, :key
715
+
716
+ def initialize(rhost, rport, key, root, listen_ip, factory, state)
717
+ debug "Instantiating a new handler:"
718
+ debug " rhost = #{rhost}"
719
+ debug " rport = #{rport}"
720
+ debug " key = #{key}"
721
+ debug " root = #{root}"
722
+ debug " listen_ip = #{listen_ip}"
723
+ debug " state = #{state}"
551
724
  super()
552
725
  @host = rhost
553
726
  @port = rport
554
727
  @key = key
555
728
  @root = root
729
+ @listen_ip = listen_ip
730
+ @factory = factory
731
+ @state = state
732
+ @filename = nil
733
+ @file = nil
734
+ @mode = nil
735
+ @blocknumber = 0
736
+ @buffer = ""
737
+ @timesent = 0
738
+
739
+ @sock = get_socket()
740
+ end
741
+
742
+ # This method returns a UDP socket bound to a random, hopefully unused
743
+ # port.
744
+ def get_socket
745
+ sock = UDPSocket.new
746
+ sock.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEADDR, true)
747
+
748
+ count = 0
749
+ bound = false
750
+ begin
751
+ port = rand(2 ** 16 - 1024) + 1024
752
+ debug "Attempting to bind to #{@listen_ip}:#{port}"
753
+ sock.bind(@listen_ip, port)
754
+ bound = true
755
+ rescue
756
+ warn "Failed to bind to #{@listen_ip}:#{port}"
757
+ if count < MaxBindAttempts
758
+ count += 1
759
+ redo
760
+ end
761
+ end
762
+
763
+ if bound
764
+ info "Handler #{@key} bound to #{@listen_ip} on port #{port}"
765
+ else
766
+ raise TftpError, "Can't seem to find a spare port to bind to."
767
+ end
768
+
769
+ return sock
770
+ end
771
+
772
+ # This method "informs" the handler that it has data waiting on its socket
773
+ # that it must read. Optionally, an already read packet can be passed to
774
+ # this method, in which case the handler will assume that the source of
775
+ # the packet has already been validated.
776
+ #
777
+ # To shut down a handler, the handler raises an exception to the server
778
+ # instance. TftpError is used to signal errors, while TftpSuccess is used
779
+ # to signal success.
780
+ # FIXME - this method is too big
781
+ def handle(*pkt)
782
+ recvpkt = nil
783
+ if pkt.length > 0
784
+ # handle passed data - we're trusting the Server to validate the
785
+ # client here.
786
+ recvpkt = pkt.shift
787
+ else
788
+ # need to read from our socket - UDP should ensure full reads
789
+ msg, sender = @sock.recvfrom(MaxBlkSize)
790
+ prot, rport, rhost, rip = sender
791
+ if rport != @port or rhost != @host
792
+ senderror(@sock,
793
+ TftpErrorType.unknownTID,
794
+ rhost,
795
+ rport)
796
+ error "Handler #{@key} received traffic from #{rhost}:#{rport} " +
797
+ "but we're talking to client #{@host}:#{@port}"
798
+ return
799
+ end
800
+
801
+ recvpkt = @factory.parse(msg)
802
+ end
803
+
804
+ # Process the packet based on its type and our state.
805
+ if recvpkt.is_a? TftpPacketRRQ
806
+ info "Received an RRQ from #{@host}:#{@port}"
807
+ # If we're starting a download, we should be in state rrq.
808
+ if @state == :rrq
809
+ # Store the filename and mode.
810
+ @filename = recvpkt.filename
811
+ info "Requested filename is #{@filename}"
812
+
813
+ @mode = recvpkt.mode
814
+ info "Requested mode is #{@mode}"
815
+
816
+ # FIXME - We only support octet mode for now.
817
+ if @mode != 'octet'
818
+ senderror(@sock,
819
+ TftpErrorType.illegalTftpOp,
820
+ @host,
821
+ @port)
822
+ @errors += 1
823
+ raise TftpError, "Unsupported mode: #{@mode}"
824
+ end
825
+
826
+ # If there are options, negotiate.
827
+ if recvpkt.options.length > 0
828
+ debug "There are options in the RRQ: #{recvpkt.options}"
829
+ # The only option we support is blksize.
830
+ if recvpkt.options.key?(:blksize)
831
+ # Note, we only consider ourselves in oack state if
832
+ # there were supported options.
833
+ debug "Client requested blksize #{@options[:blksize]}"
834
+ if TftpPacketRRQ.valid_blocksize?(recvpkt.options[:blksize])
835
+ @state = :oack
836
+ @options[:blksize] = recvpkt.options[:blksize]
837
+ else
838
+ error "Invalid blocksize #{recvpkt.options[:blksize]}"
839
+ @errors += 1
840
+ senderror(@sock,
841
+ TftpErrorType.illegalTftpOp,
842
+ @host,
843
+ @port)
844
+ end
845
+ end
846
+ end
847
+
848
+ # Do we need to send an oack? If we're in oack state, and
849
+ # we've set any supported options, then yes, we do.
850
+ if @state == :oack
851
+ oack = TftpPacketOACK.new
852
+ oack.options = @options
853
+ @sock.send(oack.encode.buffer, 0, @host, @port)
854
+ @timesent = Time.now
855
+ else
856
+ start_download()
857
+ end
858
+
859
+ else
860
+ senderror(@sock,
861
+ TftpErrorType.illegalTftpOp,
862
+ @host,
863
+ @port)
864
+ error "Received an rrq from #{@host}:#{@port}, but we're in state #{@state}"
865
+ @errors += 1
866
+ return
867
+ end
868
+
869
+ elsif recvpkt.is_a? TftpPacketWRQ
870
+ # FIXME - check state here
871
+ senderror(@sock, TftpErrorType.illegalTftpOp, @host, @port)
872
+ @errors += 1
873
+ raise TftpError, "WRQ not yet supported"
874
+
875
+ elsif recvpkt.is_a? TftpPacketACK
876
+ # If we're in state oack and the blocknumber is 0, it's an ACK to
877
+ # the OACK. Otherwise, it's an ACK to a DAT packet...hopefully.
878
+ if @state == :oack and recvpkt.blocknumber == 0
879
+ info "OACK acknowledged by client. Starting download."
880
+ start_download()
881
+ elsif @state == :dat or @state == :fin
882
+ debug "Received ACK to block #{recvpkt.blocknumber}"
883
+ if recvpkt.blocknumber == @blocknumber
884
+ if @state == :fin
885
+ raise TftpSuccess, "Successful transfer."
886
+ else
887
+ debug "Received valid ACK. Sending next DAT."
888
+ send_dat()
889
+ end
890
+ elsif recvpkt.blocknumber < @blocknumber
891
+ warn "Received duplicate ACK for block #{recvpkt.blocknumber}"
892
+ @dups += 1
893
+ return
894
+ else
895
+ error "Received ACK from the future, block #{recvpkt.blocknumber}"
896
+ @errors += 1
897
+ return
898
+ end
899
+ else
900
+ senderror(@sock,
901
+ TftpErrorType.illegalTftpOp,
902
+ @host,
903
+ @port)
904
+ @errors += 1
905
+ raise TftpError, "Received invalid ACK from client"
906
+ end
907
+
908
+ elsif recvpkt.is_a? TftpPacketERR
909
+ @errors += 1
910
+ raise TftpError, "Received error packet from client: #{recvpkt}"
911
+ else
912
+ # Unsupported packet type
913
+ senderror(@sock,
914
+ TftpErrorType.illegalTftpOp,
915
+ @host,
916
+ @port)
917
+ @errors += 1
918
+ raise TftpError, "Unsupported packet type in server: #{recvpkt}"
919
+ end
920
+ end
921
+
922
+ # This method validates the current filename requested, and initiates the
923
+ # download if everything is ok.
924
+ def start_download
925
+ @state = :dat
926
+ # Only check if there are any slashes in the filename.
927
+ if @filename =~ %r{^/}
928
+ senderror(main_sock,
929
+ TftpErrorType.illegalTftpOp,
930
+ rhost,
931
+ rport)
932
+ raise TftpError, "Absolute paths in filenames not permitted"
933
+ elsif @filename =~ %r{../}
934
+ senderror(main_sock,
935
+ TftpErrorType.illegalTftpOp,
936
+ rhost,
937
+ rport)
938
+ raise TftpError, ".. not permitted in filenames"
939
+ elsif @filename =~ %r{/}
940
+ # Make sure it's in our root.
941
+ @filename = File.expand_path(@filename)
942
+ unless @filename =~ /^@root/
943
+ # It's not in our root. Send an error.
944
+ senderror(main_sock,
945
+ TftpErrorType.illegalTftpOp,
946
+ rhost,
947
+ rport)
948
+ raise TftpError, "File request for #{@filename} outside of root"
949
+ end
950
+ end
951
+
952
+ # If it's ok, open the file and send the first DAT.
953
+ path = @root + '/' + @filename
954
+ if File.exists?(path)
955
+ debug "Opening file #{path} for reading"
956
+ @file = File.new(path, "r")
957
+ debug "File open: #{@file.inspect}"
958
+ send_dat()
959
+ else
960
+ senderror(sock,
961
+ TftpErrorType.fileNotFound,
962
+ @host,
963
+ @port)
964
+ raise TftpError, "File does not exist"
965
+ end
556
966
  end
557
967
 
558
- def handle(pkt)
968
+ # This method sends a single DAT packet, the next one in the series.
969
+ # It takes an optional :resend parameter, in which case it resends the
970
+ # last DAT instead of sending the next one.
971
+ def send_dat(*args)
972
+ debug "send_dat: args is #{args}"
973
+ opts = {}
974
+ if args.length > 0 and args[0].class == 'Hash'
975
+ opts = args[0]
976
+ end
977
+
978
+ unless opts.key?(:resend) and opts[:resend]
979
+ blksize = @options[:blksize].to_i
980
+ debug "Reading #{blksize} bytes from file #{@filename}"
981
+ rv = @file.read(blksize, @buffer)
982
+ debug "@buffer is now #{@buffer.class}"
983
+ debug "Read #{@buffer.length} bytes into buffer"
984
+ unless rv
985
+ info "End of file #{@filename} detected."
986
+ @file.close
987
+ @state = :fin
988
+ end
989
+
990
+ @blocknumber += 1
991
+ if @blocknumber > MaxBlockNum
992
+ debug "Blocknumber rolled over to zero"
993
+ @blocknumber = 0
994
+ end
995
+ else
996
+ warn "Resending block number #{@blocknumber}"
997
+ end
998
+
999
+ dat = TftpPacketDAT.new
1000
+ dat.data = @buffer
1001
+ dat.blocknumber = @blocknumber
1002
+ debug "Sending DAT packet #{@blocknumber}"
1003
+ @sock.send(dat.encode.buffer, 0, @host, @port)
1004
+ @timesent = Time.now
1005
+ end
1006
+
1007
+ # This method handles the timeout case, where the handler has send a
1008
+ # packet to the client and not received a response.
1009
+ def timeout
1010
+ @dups += 1
1011
+ send_dat(:resend => true)
1012
+ # FIXME - need to give up eventually!
559
1013
  end
560
1014
  end
561
1015
 
@@ -574,16 +1028,19 @@ class TftpClient < TftpSession
574
1028
  @address = Resolv::IPv4.create(@host)
575
1029
  rescue ArgumentError => details
576
1030
  # 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)
1031
+ begin
1032
+ @address = Resolv::IPv4.create(TCPSocket.gethostbyname(@host)[3])
1033
+ rescue SocketError => details
1034
+ # Reraise the exception for now.
1035
+ raise
1036
+ end
580
1037
  end
581
1038
  end
582
1039
 
583
1040
  # FIXME - this method is too big
584
1041
  def download(filename, output, options={})
585
- @blksize = options[:blksize] if options.has_key? :blksize
586
- $tftplog.debug('tftp+') { "Opening output file #{output}" }
1042
+ @options[:blksize] = options[:blksize] if options.has_key?(:blksize)
1043
+ debug "Opening output file #{output}"
587
1044
  fout = File.open(output, "w")
588
1045
  sock = UDPSocket.new
589
1046
 
@@ -591,8 +1048,8 @@ class TftpClient < TftpSession
591
1048
  pkt.filename = filename
592
1049
  pkt.mode = 'octet' # FIXME - shouldn't hardcode this
593
1050
  pkt.options = options
594
- $tftplog.info('tftp+') { "Sending download request for #{filename}" }
595
- $tftplog.info('tftp+') { "host = #{@host}, port = #{@iport}" }
1051
+ info "Sending download request for #{filename}"
1052
+ info "host = #{@host}, port = #{@iport}"
596
1053
  sock.send(pkt.encode.buffer, 0, @host, @iport)
597
1054
  @state = :rrq
598
1055
 
@@ -601,7 +1058,7 @@ class TftpClient < TftpSession
601
1058
  blocknumber = 1
602
1059
  retry_count = 0
603
1060
  loop do
604
- $tftplog.debug('tftp+') { "Waiting for incoming datagram..." }
1061
+ debug "Waiting for incoming datagram..."
605
1062
  msg = sender = nil
606
1063
  begin
607
1064
  status = Timeout::timeout(SockTimeout) {
@@ -611,60 +1068,64 @@ class TftpClient < TftpSession
611
1068
  retry_count += 1
612
1069
  if retry_count > MaxRetry
613
1070
  msg = "Timeout! Max retries exceeded. Giving up."
614
- $tftplog.error('tftp+') { msg }
1071
+ error msg
615
1072
  raise TftpError, msg
616
1073
  else
617
- $tftplog.debug('tftp+') { "Timeout! Lets try again." }
1074
+ debug "Timeout! Lets try again."
618
1075
  next
619
1076
  end
620
1077
  end
621
1078
  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}" }
1079
+ info "Received #{msg.length} byte packet"
1080
+ debug "Remote port is #{rport} and remote host is #{rhost}"
624
1081
 
625
1082
  if @address.to_s != rip
626
1083
  # Skip it
627
1084
  @errors += 1
628
- $stderr.write "It is a rogue packet! #{sender[1]} #{sender[2]}\n"
1085
+ error "It is a rogue packet! #{sender[1]} #{sender[2]}"
629
1086
  next
630
1087
  elsif @port and @port != rport.to_s
631
1088
  # Skip it
632
1089
  @errors += 1
633
- $stderr.write "It is a rogue packet! #{sender[1]} #{sender[2]}\n"
1090
+ error "It is a rogue packet! #{sender[1]} #{sender[2]}"
634
1091
  next
635
1092
  else not @port
636
1093
  # Set this as our TID
637
- $tftplog.info('tftp+') { "Set remote TID to #{@port}" }
1094
+ debug "@port was #{@port}"
638
1095
  @port = rport.to_s
1096
+ info "Set remote TID to #{@port}"
639
1097
  end
640
1098
 
641
1099
  pkt = factory.parse(msg)
642
- $tftplog.debug('tftp+') { "pkt is #{pkt}" }
1100
+ debug "pkt is #{pkt}"
643
1101
 
644
1102
  # FIXME - Refactor this into separate methods to handle each case.
645
1103
  if pkt.is_a? TftpPacketRRQ
646
1104
  # 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)
1105
+ senderror(sock,
1106
+ TftpErrorType.illegalTftpOp,
1107
+ @host,
1108
+ @port)
650
1109
  @errors += 1
651
- $stderr.write "It is a RRQ packet in download, state #{@state}\n"
1110
+ debug "It is a RRQ packet in download, state #{@state}"
652
1111
 
653
1112
  elsif pkt.is_a? TftpPacketWRQ
654
1113
  # 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)
1114
+ senderror(sock,
1115
+ TftpErrorType.illegalTftpOp,
1116
+ @host,
1117
+ @port)
658
1118
  @errors += 1
659
- $stderr.write "It is a WRQ packet in download, state #{@state}\n"
1119
+ debug "It is a WRQ packet in download, state #{@state}"
660
1120
 
661
1121
  elsif pkt.is_a? TftpPacketACK
662
1122
  # 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)
1123
+ senderror(sock,
1124
+ TftpErrorType.illegalTftpOp,
1125
+ @host,
1126
+ @port)
666
1127
  @errors += 1
667
- $stderr.write "It is a ACK packet in download, state #{@state}\n"
1128
+ debug "It is a ACK packet in download, state #{@state}"
668
1129
 
669
1130
  elsif pkt.is_a? TftpPacketERR
670
1131
  @errors += 1
@@ -672,8 +1133,12 @@ class TftpClient < TftpSession
672
1133
 
673
1134
  elsif pkt.is_a? TftpPacketOACK
674
1135
  unless @state == :rrq
1136
+ senderror(sock,
1137
+ TftpErrorType.illegalTftpOp,
1138
+ @host,
1139
+ @port)
675
1140
  @errors += 1
676
- $stderr.write "It is a OACK in state #{@state}"
1141
+ debug "It is an OACK in state #{@state}"
677
1142
  next
678
1143
  end
679
1144
 
@@ -685,11 +1150,12 @@ class TftpClient < TftpSession
685
1150
  case optname
686
1151
  when :blksize
687
1152
  # The blocksize can be <= what we proposed.
688
- unless options.has_key? :blksize
1153
+ unless options.has_key?(:blksize)
689
1154
  # 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)
1155
+ senderror(sock,
1156
+ TftpErrorType.failedNegotiation,
1157
+ @host,
1158
+ @port)
693
1159
  raise TftpError, "It is a OACK with blocksize when we didn't ask for one."
694
1160
  end
695
1161
 
@@ -701,9 +1167,10 @@ class TftpClient < TftpSession
701
1167
  # FIXME - refactor err packet handling from above...
702
1168
  # Nothing that we don't know of should be in the
703
1169
  # oack packet.
704
- err = TftpPacketERR.new
705
- err.errorcode = 8 # failed negotiation
706
- sock.send(err.encode.buffer, 0, @host, @port)
1170
+ senderror(sock,
1171
+ TftpErrorType.failedNegotiation,
1172
+ @host,
1173
+ @port)
707
1174
  raise TftpError, "Failed to negotiate options: #{pkt.options}"
708
1175
  end
709
1176
  end
@@ -717,9 +1184,10 @@ class TftpClient < TftpSession
717
1184
  @state = :ack
718
1185
  else
719
1186
  # OACK with no options?
720
- err = TftpPacketERR.new
721
- err.errorcode = 8 # failed negotiation
722
- sock.send(err.encode.buffer, 0, @host, @port)
1187
+ senderror(sock,
1188
+ TftpErrorType.failedNegotiation,
1189
+ @host,
1190
+ @port)
723
1191
  raise TftpError, "OACK with no options"
724
1192
  end
725
1193
 
@@ -727,7 +1195,7 @@ class TftpClient < TftpSession
727
1195
  # to send an ACK to the server, with block number 0.
728
1196
  ack = TftpPacketACK.new
729
1197
  ack.blocknumber = 0
730
- $tftplog.info('tftp+') { "Sending ACK to OACK" }
1198
+ info "Sending ACK to OACK"
731
1199
  sock.send(ack.encode.buffer, 0, @host, @port)
732
1200
  @state = :ack
733
1201
 
@@ -736,34 +1204,35 @@ class TftpClient < TftpSession
736
1204
  # server didn't send us an oack, and the options were refused.
737
1205
  # FIXME - we need to handle all possible options and set them
738
1206
  # back to their defaults here, not just blocksize.
739
- if @state == :rrq and options.has_key? :blksize
740
- @blksize = DefBlkSize
1207
+ if @state == :rrq and options.has_key?(:blksize)
1208
+ @options[:blksize] = DefBlkSize
741
1209
  end
742
1210
 
743
1211
  @state = :dat
744
- $tftplog.info('tftp+') { "It is a DAT packet, block #{pkt.blocknumber}" }
745
- $tftplog.debug('tftp+') { "DAT size is #{pkt.data.length}" }
1212
+ info "It is a DAT packet, block #{pkt.blocknumber}"
1213
+ debug "DAT size is #{pkt.data.length}"
746
1214
 
747
1215
  ack = TftpPacketACK.new
748
1216
  ack.blocknumber = pkt.blocknumber
749
1217
 
750
- $tftplog.info('tftp+') { "Sending ACK to block #{ack.blocknumber}" }
1218
+ info "Sending ACK to block #{ack.blocknumber}"
751
1219
  sock.send(ack.encode.buffer, 0, @host, @port)
752
1220
 
753
1221
  # Check for dups
754
1222
  if pkt.blocknumber <= blocknumber
755
- $tftplog.warn('tftp+') { "It is a DUP for block #{blocknumber}" }
1223
+ warn "It is a DUP for block #{blocknumber}"
756
1224
  @dups += 1
757
1225
  elsif pkt.blocknumber = blocknumber+1
758
- $tftplog.debug('tftp+') { "It is a properly ordered DAT packet" }
1226
+ debug "It is a properly ordered DAT packet"
759
1227
  blocknumber += 1
760
1228
  else
761
1229
  # 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)
1230
+ senderror(sock,
1231
+ TftpErrorType.illegalTftpOp,
1232
+ @host,
1233
+ @port)
765
1234
  @errors += 1
766
- $stderr.write "It is a future packet!\n"
1235
+ debug "It is a future packet!"
767
1236
  end
768
1237
 
769
1238
  # Call any block passed.
@@ -774,16 +1243,16 @@ class TftpClient < TftpSession
774
1243
  # Write the data to the file.
775
1244
  fout.print pkt.data
776
1245
  # 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." }
1246
+ debug "pkt.data.length is #{pkt.data.length}"
1247
+ if pkt.data.length < @options[:blksize]
1248
+ info "It is a last packet."
780
1249
  fout.close
781
1250
  @state = :done
782
1251
  break
783
1252
  end
784
1253
  else
785
1254
  msg = "It is an unknown packet: #{pkt}"
786
- $tftplog.error('tftp+') { msg }
1255
+ error msg
787
1256
  raise TftpError, msg
788
1257
  end
789
1258
  end
data/test/test.rb CHANGED
@@ -4,8 +4,19 @@ $:.unshift File.join(File.dirname(__FILE__), "..", "lib")
4
4
  require 'net/tftp+'
5
5
  require 'test/unit'
6
6
 
7
+ # Mock socket class
8
+ # FIXME - it should probably do more than this
9
+ class MockSock
10
+ def send(*args)
11
+ return true
12
+ end
13
+ end
14
+
7
15
  class TestTftp < Test::Unit::TestCase
8
- def test_simple
16
+ def test_setup
17
+ end
18
+
19
+ def test_rrq
9
20
  rrq = TftpPacketRRQ.new
10
21
  rrq.filename = 'myfilename'
11
22
  rrq.mode = 'octet'
@@ -15,7 +26,9 @@ class TestTftp < Test::Unit::TestCase
15
26
  rrq.decode
16
27
  assert_equal('myfilename', rrq.filename)
17
28
  assert_equal('octet', rrq.mode)
18
-
29
+ end
30
+
31
+ def test_wrq
19
32
  wrq = TftpPacketWRQ.new
20
33
  wrq.buffer = "\000\002myfilename\000octet\000"
21
34
  wrq.decode
@@ -26,7 +39,9 @@ class TestTftp < Test::Unit::TestCase
26
39
  assert_equal('myfilename', wrq.filename)
27
40
  assert_equal('octet', wrq.mode)
28
41
  assert_equal(2, wrq.opcode)
29
-
42
+ end
43
+
44
+ def test_dat
30
45
  dat = TftpPacketDAT.new
31
46
  sampledat = "\000\001\002\003\004\005"
32
47
  dat.data = sampledat
@@ -34,18 +49,24 @@ class TestTftp < Test::Unit::TestCase
34
49
  assert_equal(sampledat, dat.data)
35
50
  assert_equal(6, dat.data.length)
36
51
  assert_equal(3, dat.opcode)
37
-
52
+ end
53
+
54
+ def test_ack
38
55
  ack = TftpPacketACK.new
39
56
  ack.blocknumber = 5
40
57
  assert_equal(4, ack.opcode)
41
58
  assert_equal(5, ack.encode.decode.blocknumber)
42
-
59
+ end
60
+
61
+ def test_err
43
62
  err = TftpPacketERR.new
44
63
  err.errorcode = 3
45
64
  assert_equal('Disk full or allocation exceeded.',
46
65
  err.encode.decode.errmsg)
47
66
  assert_equal(5, err.opcode)
48
-
67
+ end
68
+
69
+ def test_oack
49
70
  oack = TftpPacketOACK.new
50
71
  oack_options = {
51
72
  :blksize => 4096
@@ -55,4 +76,48 @@ class TestTftp < Test::Unit::TestCase
55
76
  assert_equal('4096', oack.options[:blksize])
56
77
  assert_equal(6, oack.opcode)
57
78
  end
79
+
80
+ def test_errortype
81
+ assert_equal(0, TftpErrorType.notDefined)
82
+ assert_equal(1, TftpErrorType.fileNotFound)
83
+ assert_equal(2, TftpErrorType.accessViolation)
84
+ assert_equal(3, TftpErrorType.diskFull)
85
+ assert_equal(4, TftpErrorType.illegalTftpOp)
86
+ assert_equal(5, TftpErrorType.unknownTID)
87
+ assert_equal(6, TftpErrorType.fileAlreadyExists)
88
+ assert_equal(7, TftpErrorType.noSuchUser)
89
+ assert_equal(8, TftpErrorType.failedNegotiation)
90
+ end
91
+
92
+ def test_packetfactory
93
+ factory = TftpPacketFactory.new
94
+ rrq = factory.create(1)
95
+ wrq = factory.create(2)
96
+ dat = factory.create(3)
97
+ ack = factory.create(4)
98
+ err = factory.create(5)
99
+ oack = factory.create(6)
100
+ assert_equal(true, rrq.is_a?(TftpPacketRRQ))
101
+ assert_equal(true, wrq.is_a?(TftpPacketWRQ))
102
+ assert_equal(true, dat.is_a?(TftpPacketDAT))
103
+ assert_equal(true, ack.is_a?(TftpPacketACK))
104
+ assert_equal(true, err.is_a?(TftpPacketERR))
105
+ assert_equal(true, oack.is_a?(TftpPacketOACK))
106
+ assert_raise(ArgumentError) { factory.create(0) }
107
+ assert_raise(ArgumentError) { factory.create(7) }
108
+ assert_raise(ArgumentError) { factory.parse(TftpPacketRRQ.new) }
109
+ assert_raise(ArgumentError) { factory.parse(TftpPacketRRQ.new.buffer) }
110
+ assert_raise(TftpError) { factory.parse("foobar") }
111
+ end
112
+
113
+ def test_session
114
+ session = TftpSession.new
115
+ assert_equal(512, session.options[:blksize])
116
+ assert_equal(nil, session.state)
117
+ assert_equal(0, session.dups)
118
+ assert_equal(0, session.errors)
119
+
120
+ sock = MockSock.new
121
+ assert_equal(true, session.senderror(sock, 1, '192.168.0.1', '69'))
122
+ end
58
123
  end
metadata CHANGED
@@ -1,10 +1,10 @@
1
1
  --- !ruby/object:Gem::Specification
2
- rubygems_version: 0.9.0
2
+ rubygems_version: 0.9.2
3
3
  specification_version: 1
4
4
  name: tftpplus
5
5
  version: !ruby/object:Gem::Version
6
- version: "0.2"
7
- date: 2006-12-08 00:00:00 -05:00
6
+ version: "0.3"
7
+ date: 2007-03-19 00:00:00 -04:00
8
8
  summary: A pure tftp implementation with support for variable block sizes
9
9
  require_paths:
10
10
  - lib