net-tftp 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (2) hide show
  1. data/lib/net/tftp.rb +294 -0
  2. metadata +37 -0
@@ -0,0 +1,294 @@
1
+ #! /usr/bin/ruby
2
+
3
+ #--
4
+ # Copyright (c) 2004, Guillaume Marcais (guillaume.marcais@free.fr)
5
+ # All rights reserved.
6
+ # This file is distributed under the Ruby license.
7
+ # http://net-tftp.rubyforge.org
8
+ #++
9
+ #
10
+ # == Description
11
+ # TFTP is used by many devices to upload or download their configuration,
12
+ # firmware or else. It is a very simple file transfer protocol built on top
13
+ # of UDP. It transmits data by chunck of 512 bytes. It waits for an ack after
14
+ # each data packet but does not do any data integrety checks.
15
+ # There is no authentication mechanism nor any way too list the content of
16
+ # the remote directories. It just sends or retrieves files.
17
+ #
18
+ # == Usage
19
+ # Using TFTP is fairly trivial:
20
+ # <pre>
21
+ # <code>
22
+ # require 'net/tftp'
23
+ # t = Net::TFTP.new('localhost')
24
+ # t.getbinaryfile('remote_file', 'local_file')
25
+ # t.putbinaryfile('local_file', 'remote_file')
26
+ # </code>
27
+ # </pre>
28
+ #
29
+ # That's pretty much it. +getbinaryfile+ and +putbinaryfile+ can take a
30
+ # block which will be called every time a block is sent/received.
31
+ #
32
+ # == Known limitations
33
+ # * RFC 1350 mention a net-ascii mode. I am not quite sure what transformation
34
+ # on the data should be done and it is not (yet) implemented.
35
+ # * None of the extensions of TFTP are implemented (RFC1782, RFC1783, RFC1784,
36
+ # RFC1785, RFC2347, RFC2348, RFC2349).
37
+
38
+ require 'socket'
39
+ require 'timeout'
40
+
41
+ module Net # :nodoc:
42
+
43
+ class TFTPError < StandardError; end
44
+ class TFTPTimeout < TFTPError; end
45
+ class TFTPProtocol < TFTPError
46
+ attr_reader :code
47
+ def initialize(msg, code)
48
+ super(msg)
49
+ @code = code
50
+ end
51
+ end
52
+
53
+
54
+ class TFTP
55
+ VERSION = "0.1.0"
56
+ DEFAULTS = {
57
+ :port => (Socket.getservbyname("tftp", "udp") rescue 69),
58
+ :timeout => 5,
59
+ }
60
+
61
+ MINSIZE = 4
62
+ MAXSIZE = 516
63
+ DATABLOCK = 512
64
+
65
+ # Errors
66
+ ERROR_DESCRIPTION = [
67
+ "Custom error",
68
+ "File not found",
69
+ "Access violation",
70
+ "Disk full",
71
+ "Illegal TFP operation",
72
+ "Unknown transfer ID",
73
+ "File already exists",
74
+ "No such user",
75
+ ]
76
+ ERROR_UNDEF = 0
77
+ ERROR_FILE_NOT_FOUND = 1
78
+ ERROR_ACCESS_VIOLATION = 2
79
+ ERROR_DISK_FULL = 3
80
+ ERROR_ILLEGAL_OPERATION = 4
81
+ ERROR_UNKNOWN_TRANSFER_ID = 5
82
+ ERROR_FILE_ALREADY_EXISTS = 6
83
+ ERROR_NO_SUCH_USER = 7
84
+
85
+ # Opcodes
86
+ OP_RRQ = 1
87
+ OP_WRQ = 2
88
+ OP_DATA = 3
89
+ OP_ACK = 4
90
+ OP_ERROR = 5
91
+
92
+ class << self
93
+ # Alias for new
94
+ def open(host)
95
+ new(host)
96
+ end
97
+
98
+ # Return the number of blocks to send _size_ bytes.
99
+ def size_in_blocks(size)
100
+ s = size / DATABLOCK
101
+ s += 1 unless (size % DATABLOCK) == 0
102
+ s
103
+ end
104
+ end
105
+
106
+ attr_accessor :timeout, :host
107
+
108
+ # Create a TFTP connection object to a host. Note that no actual
109
+ # network connection is made. This methods never fails.
110
+ # Parameters:
111
+ # [:port] The UDP port. See DEFAULTS
112
+ # [:timeout] Timeout in second for each ack packet. See DEFAULTS
113
+ def initialize(host, params = {})
114
+ @host = host
115
+ @port = params[:port] || DEFAULTS[:port]
116
+ @timeout = params[:timeout] || DEFAULTS[:timeout]
117
+ end
118
+
119
+ # Retrieve a file using binary mode.
120
+ # If the localfile name is omitted, it is set to the remotefile.
121
+ # The optional block receives the data in the block and the sequence number
122
+ # of the block starting at 1.
123
+ def getbinaryfile(remotefile, localfile = nil, &block) # :yields: data, seq
124
+ localfile ||= File.basename(remotefile)
125
+ open(localfile, "w") do |f|
126
+ getbinary(remotefile, f, &block)
127
+ end
128
+ end
129
+
130
+ # Retrieve a file using binary mode and send content to an io object
131
+ # The optional block receives the data in the block and the sequence number
132
+ # of the block starting at 1.
133
+ def getbinary(remotefile, io, &block) # :yields: data, seq
134
+ s = UDPSocket.new
135
+ begin
136
+ peer_ip = IPSocket.getaddress(@host)
137
+ rescue
138
+ raise TFTPError, "Cannot find host '#{@host}'"
139
+ end
140
+
141
+ peer_tid = nil
142
+ seq = 1
143
+ from = nil
144
+ data = nil
145
+
146
+ # Initialize request
147
+ s.send(rrq_packet(remotefile, "octet"), 0, peer_ip, @port)
148
+ Timeout::timeout(@timeout, TFTPTimeout) do
149
+ loop do
150
+ packet, from = s.recvfrom(MAXSIZE, 0)
151
+ next unless peer_ip == from[3]
152
+ type, block, data = scan_packet(packet)
153
+ break if (type == OP_DATA) && (block == seq)
154
+ end
155
+ end
156
+ peer_tid = from[1]
157
+
158
+ # Get and write data to io
159
+ loop do
160
+ io.write(data)
161
+ s.send(ack_packet(seq), 0, peer_ip, peer_tid)
162
+ yield(data, seq) if block_given?
163
+ break if data.size < DATABLOCK
164
+
165
+ seq += 1
166
+ Timeout::timeout(@timeout, TFTPTimeout) do
167
+ loop do
168
+ packet, from = s.recvfrom(MAXSIZE, 0)
169
+ next unless peer_ip == from[3]
170
+ if peer_tid != from[1]
171
+ s.send(error_packet(ERROR_UNKNOWN_TRANSFER_ID),
172
+ 0, from[3], from[1])
173
+ next
174
+ end
175
+ type, block, data = scan_packet(packet)
176
+ break if (type == OP_DATA) && (block == seq)
177
+ end
178
+ end
179
+ end
180
+
181
+ return true
182
+ end
183
+
184
+ # Send a file in binary mode. The name of the remotefile is set to
185
+ # the name of the local file if omitted.
186
+ # The optional block receives the data in the block and the sequence number
187
+ # of the block starting at 1.
188
+ def putbinaryfile(localfile, remotefile = nil, &block) # :yields: data, seq
189
+ remotefile ||= File.basename(localfile)
190
+ open(localfile) do |f|
191
+ putbinary(remotefile, f, &block)
192
+ end
193
+ end
194
+
195
+ # Send the content read from io to the remotefile.
196
+ # The optional block receives the data in the block and the sequence number
197
+ # of the block starting at 1.
198
+ def putbinary(remotefile, io, &block) # :yields: data, seq
199
+ s = UDPSocket.new
200
+ peer_ip = IPSocket.getaddress(@host)
201
+
202
+ peer_tid = nil
203
+ seq = 0
204
+ from = nil
205
+ data = nil
206
+
207
+ # Initialize request
208
+ s.send(wrq_packet(remotefile, "octet"), 0, peer_ip, @port)
209
+ Timeout::timeout(@timeout, TFTPTimeout) do
210
+ loop do
211
+ packet, from = s.recvfrom(MAXSIZE, 0)
212
+ next unless peer_ip == from[3]
213
+ type, block, data = scan_packet(packet)
214
+ break if (type == OP_ACK) && (block == seq)
215
+ end
216
+ end
217
+ peer_tid = from[1]
218
+
219
+ loop do
220
+ data = io.read(DATABLOCK) || ""
221
+ seq += 1
222
+ s.send(data_packet(seq, data), 0, peer_ip, peer_tid)
223
+
224
+ Timeout::timeout(@timeout, TFTPTimeout) do
225
+ loop do
226
+ packet, from = s.recvfrom(MAXSIZE, 0)
227
+ next unless peer_ip == from[3]
228
+ if peer_tid != from[1]
229
+ s.send(error_packet(ERROR_UNKNOWN_TRANSFER_ID),
230
+ 0, from[3], from[1])
231
+ next
232
+ end
233
+ type, block, void = scan_packet(packet)
234
+ break if (type == OP_ACK) && (block == seq)
235
+ end
236
+ end
237
+
238
+ yield(data, seq) if block_given?
239
+ break if data.size < DATABLOCK
240
+ end
241
+
242
+ return true
243
+ end
244
+
245
+ ####################
246
+ # Private methods #
247
+ ####################
248
+ private
249
+ def rrq_packet(file, mode)
250
+ [OP_RRQ, file, mode].pack("na#{file.size + 1}a#{mode.size + 1}")
251
+ end
252
+
253
+ def wrq_packet(file, mode)
254
+ [OP_WRQ, file, mode].pack("na#{file.size + 1}a#{mode.size + 1}")
255
+ end
256
+
257
+ def data_packet(block, data)
258
+ [OP_DATA, block, data].pack("nna*")
259
+ end
260
+
261
+ def ack_packet(block)
262
+ [OP_ACK, block].pack("nn")
263
+ end
264
+
265
+ def error_packet(code, message = nil)
266
+ message ||= ERROR_DESCRIPTION[code] || ""
267
+ [OP_ERROR, code, message].pack("nna#{message.size + 1}")
268
+ end
269
+
270
+ # Check if the packet is malformed (unknown opcode, too big, etc.),
271
+ # in which case it returns nil.
272
+ # If it is an error packet, raise an TFTPProtocol error.
273
+ # Returns scanned values otherwise.
274
+ def scan_packet(packet)
275
+ return nil if packet.size < MINSIZE || packet.size > MAXSIZE
276
+ opcode, block_err, rest = packet.unpack("nna*")
277
+ return nil if opcode.nil? || block_err.nil?
278
+ case opcode
279
+ when OP_RRQ, OP_WRQ
280
+ return nil
281
+ when OP_DATA
282
+ return [opcode, block_err, rest]
283
+ when OP_ACK
284
+ return [opcode, block_err]
285
+ when OP_ERROR
286
+ err_msg = "%s: %s"
287
+ err_msg %= [ERROR_DESCRIPTION[block_err] || "", rest.chomp("\000")]
288
+ raise TFTPProtocol.new(err_msg, block_err)
289
+ else
290
+ return nil
291
+ end
292
+ end
293
+ end
294
+ end
metadata ADDED
@@ -0,0 +1,37 @@
1
+ --- !ruby/object:Gem::Specification
2
+ rubygems_version: 0.8.3
3
+ specification_version: 1
4
+ name: net-tftp
5
+ version: !ruby/object:Gem::Version
6
+ version: 0.1.0
7
+ date: 2005-01-07
8
+ summary: Net::TFTP is a pure Ruby implementation of the Trivial File Transfer Protocol (RFC 1350)
9
+ require_paths:
10
+ - lib
11
+ email: guillaume.marcais@free.fr
12
+ homepage: http://net-tftp.rubyforge.org
13
+ rubyforge_project:
14
+ description:
15
+ autorequire: net/tftp
16
+ default_executable:
17
+ bindir: bin
18
+ has_rdoc: true
19
+ required_ruby_version: !ruby/object:Gem::Version::Requirement
20
+ requirements:
21
+ -
22
+ - ">"
23
+ - !ruby/object:Gem::Version
24
+ version: 0.0.0
25
+ version:
26
+ platform: ruby
27
+ authors:
28
+ - Guillaume Marcais
29
+ files:
30
+ - lib/net/tftp.rb
31
+ test_files: []
32
+ rdoc_options: []
33
+ extra_rdoc_files: []
34
+ executables: []
35
+ extensions: []
36
+ requirements: []
37
+ dependencies: []