net-tftp 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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: []