tftpplus 0.2 → 0.3

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 +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