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