net-tftp 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|