tftpplus 0.2 → 0.3
Sign up to get free protection for your applications and to get access to all the features.
- data/ChangeLog +135 -19
- data/README +6 -2
- data/bin/tftp_client.rb +20 -9
- data/lib/net/tftp+.rb +577 -108
- data/test/test.rb +71 -6
- metadata +3 -3
data/ChangeLog
CHANGED
@@ -1,47 +1,163 @@
|
|
1
|
-
|
2
|
-
* Fixed handling of remote TID.
|
3
|
-
* Added tar task to Rakefile.
|
1
|
+
2007-03-15 00:00 msoulier
|
4
2
|
|
5
|
-
|
6
|
-
|
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
|
-
*
|
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
|
-
*
|
125
|
+
* lib/net/tftp+.rb: Optimizing Tftp object dispatch
|
16
126
|
|
17
127
|
2006-10-04 01:27 msoulier
|
18
128
|
|
19
|
-
*
|
129
|
+
* lib/net/tftp+.rb: minor method rename
|
20
130
|
|
21
131
|
2006-09-25 02:11 msoulier
|
22
132
|
|
23
|
-
*
|
133
|
+
* lib/net/tftp+.rb: Added timeouts on recvfrom
|
24
134
|
|
25
135
|
2006-09-24 02:39 msoulier
|
26
136
|
|
27
|
-
*
|
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
|
-
*
|
32
|
-
|
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
|
-
*
|
154
|
+
* lib/net/tftp+.rb: Pulling hardcoded test
|
37
155
|
|
38
156
|
2006-09-22 16:44 msoulier
|
39
157
|
|
40
|
-
* doc, lib, test,
|
41
|
-
branch structure.
|
158
|
+
* doc, lib, test, doc, lib, test: Setting up branch structure.
|
42
159
|
|
43
|
-
2006-09-22 16:
|
160
|
+
2006-09-22 16:44 msoulier
|
44
161
|
|
45
|
-
*
|
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', '--
|
50
|
+
opts.on('-b', '--blocksize=', 'Blocksize option: 8-65536 bytes') do |b|
|
51
51
|
options.blksize = b.to_i
|
52
|
-
$log.debug('client') { "
|
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
|
-
"
|
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:
|
90
|
+
$log.info('client') { "Options: blocksize = #{options.blksize}" }
|
92
91
|
|
93
|
-
|
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
|
-
|
96
|
-
|
97
|
-
|
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
|
-
|
84
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
460
|
-
|
461
|
-
|
462
|
-
|
463
|
-
|
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
|
-
@
|
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
|
-
@
|
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
|
-
#
|
502
|
-
def listen(port, path, iface="
|
575
|
+
# INADDR_ANY.
|
576
|
+
def listen(port, path, iface="")
|
503
577
|
@iface = iface
|
504
578
|
@port = port
|
505
579
|
@root = path
|
506
|
-
|
507
|
-
|
508
|
-
|
509
|
-
|
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
|
-
|
516
|
-
|
517
|
-
|
518
|
-
|
519
|
-
|
520
|
-
|
521
|
-
|
522
|
-
|
523
|
-
|
524
|
-
|
525
|
-
|
526
|
-
|
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
|
-
|
529
|
-
|
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
|
-
|
538
|
-
|
539
|
-
|
540
|
-
|
541
|
-
|
542
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
578
|
-
|
579
|
-
|
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?
|
586
|
-
|
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
|
-
|
595
|
-
|
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
|
-
|
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
|
-
|
1071
|
+
error msg
|
615
1072
|
raise TftpError, msg
|
616
1073
|
else
|
617
|
-
|
1074
|
+
debug "Timeout! Lets try again."
|
618
1075
|
next
|
619
1076
|
end
|
620
1077
|
end
|
621
1078
|
prot, rport, rhost, rip = sender
|
622
|
-
|
623
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
648
|
-
|
649
|
-
|
1105
|
+
senderror(sock,
|
1106
|
+
TftpErrorType.illegalTftpOp,
|
1107
|
+
@host,
|
1108
|
+
@port)
|
650
1109
|
@errors += 1
|
651
|
-
|
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
|
-
|
656
|
-
|
657
|
-
|
1114
|
+
senderror(sock,
|
1115
|
+
TftpErrorType.illegalTftpOp,
|
1116
|
+
@host,
|
1117
|
+
@port)
|
658
1118
|
@errors += 1
|
659
|
-
|
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
|
-
|
664
|
-
|
665
|
-
|
1123
|
+
senderror(sock,
|
1124
|
+
TftpErrorType.illegalTftpOp,
|
1125
|
+
@host,
|
1126
|
+
@port)
|
666
1127
|
@errors += 1
|
667
|
-
|
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
|
-
|
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?
|
1153
|
+
unless options.has_key?(:blksize)
|
689
1154
|
# Hey, we didn't ask for a blocksize option...
|
690
|
-
|
691
|
-
|
692
|
-
|
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
|
-
|
705
|
-
|
706
|
-
|
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
|
-
|
721
|
-
|
722
|
-
|
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
|
-
|
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?
|
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
|
-
|
745
|
-
|
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
|
-
|
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
|
-
|
1223
|
+
warn "It is a DUP for block #{blocknumber}"
|
756
1224
|
@dups += 1
|
757
1225
|
elsif pkt.blocknumber = blocknumber+1
|
758
|
-
|
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
|
-
|
763
|
-
|
764
|
-
|
1230
|
+
senderror(sock,
|
1231
|
+
TftpErrorType.illegalTftpOp,
|
1232
|
+
@host,
|
1233
|
+
@port)
|
765
1234
|
@errors += 1
|
766
|
-
|
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
|
-
|
778
|
-
if pkt.data.length < @blksize
|
779
|
-
|
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
|
-
|
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
|
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.
|
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.
|
7
|
-
date:
|
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
|