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.
- data/lib/net/tftp.rb +294 -0
- metadata +37 -0
data/lib/net/tftp.rb
ADDED
@@ -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: []
|