fx-tftp 0.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 39beafe0ad28d4186166ddaa02fdb501c2d303f3
4
+ data.tar.gz: 4069a701c1205324e86eae44e8153fd377ee1b0a
5
+ SHA512:
6
+ metadata.gz: 1f97391b5831198c2528ab81200c0b4175c70f1de238ceafbd03d4e5e6809f18b3568d8873db2db649163eef3615270252036733ec535a7f05dd15511346b364
7
+ data.tar.gz: b22a2355500c4cb7e7f3a9bfdb66fc7200edf70190ab05bd58baa6ab81a7aa79b6c6de13cfb13d726c154a454160882e09dd63468f7910ef378147eeaab5ed32
@@ -0,0 +1,2 @@
1
+ -
2
+ LICENSE.txt
@@ -0,0 +1,3 @@
1
+ doc/
2
+ pkg/
3
+ .yardoc/
@@ -0,0 +1,8 @@
1
+ language: ruby
2
+ rvm:
3
+ - ruby-head
4
+ - 1.9.3
5
+ install:
6
+ - gem install minitest
7
+ script:
8
+ - rake test
@@ -0,0 +1,5 @@
1
+ --title "FX-TFTP Documentation"
2
+ --markup markdown
3
+ --charset utf-8
4
+ --protected
5
+ --private
@@ -0,0 +1,23 @@
1
+ Copyright (c) 2015, Piotr S. Staszewski
2
+ All rights reserved.
3
+
4
+ Redistribution and use in source and binary forms, with or without
5
+ modification, are permitted provided that the following conditions are met:
6
+
7
+ 1. Redistributions of source code must retain the above copyright notice, this
8
+ list of conditions and the following disclaimer.
9
+
10
+ 2. Redistributions in binary form must reproduce the above copyright notice,
11
+ this list of conditions and the following disclaimer in the documentation
12
+ and/or other materials provided with the distribution.
13
+
14
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
15
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
16
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
17
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
18
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
19
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
20
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
21
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
22
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
23
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,62 @@
1
+ # FX-TFTP [![Build Status](https://travis-ci.org/drbig/fx-tftp.svg?branch=master)](https://travis-ci.org/drbig/fx-tftp) [![Gem](http://img.shields.io/gem/v/fx-tftp.svg)](https://rubygems.org/gems/fx-tftp) [![Yard Docs](http://img.shields.io/badge/yard-docs-blue.svg)](http://www.rubydoc.info/github/drbig/fx-tftp/master)
2
+
3
+ * [Homepage](https://github.com/drbig/fx-tftp)
4
+ * [Documentation](http://rubydoc.info/gems/fx-tftp/frames)
5
+
6
+ ## Description
7
+
8
+ FX-TFTP is a slightly over-OO-ed pure-Ruby implementation of plain [RFC1350](https://www.ietf.org/rfc/rfc1350.txt) TFTP *server*. It is very flexible and intended for hacking. Also, and more importantly, it **works**, contrary to other gems that are occupying space at RubyGems.
9
+
10
+ That flexibility may be useful if you're planning on massive custom TFTP-based boots, or if you're into ~~hacking~~ researching cheap router security. The request packets parsing has been relaxed so that it should work with TFTP clients that use some fancy extensions. I have tested the server on Linux x86_64 with Ruby 2.2.0 and on FreeBSD amd64 with Ruby 1.9.3p194, and it successfully exchanged data both ways with clients running on numerous platforms.
11
+
12
+ The included `tftpd` executable gives you a fully-fledged read-write TFTP server that also does logging, daemon mode and does not crap out on `SIGTERM`.
13
+
14
+ ## Hacking
15
+
16
+ Suppose we want to have a TFTP server that only supports reading, but the files served should depend on the IP block the client is connecting from:
17
+
18
+ class CustomHandler < TFTP::Handler::RWSimple
19
+ def run!(tag, req, sock, src)
20
+ if req.is_a? TFTP::Packet::WRQ
21
+ sock.send(TFTP::Packet::ERROR.new(4, 'Nope').encode, 0)
22
+ sock.close
23
+ return
24
+ end
25
+
26
+ ip = src.remote_address.ip_address.split('.')
27
+ block = ip.slice(0, 3).join('-')
28
+ req.filename = File.join(block, req.filename)
29
+ log :info, "#{tag} Mapped filename to #{req.filename}"
30
+ super(tag, req, sock, src)
31
+ end
32
+ end
33
+
34
+ srv = TFTP::Server::Base.new(CustomHandler.new(path), opts)
35
+
36
+ When you combine filename inspection and `#send` and `#recv` methods working on plain `IO` objects you can easily whip up things like serving dynamically built scripts/binaries/archives based on parameters passed as the requested 'filename'.
37
+
38
+ ## Included executable
39
+
40
+ $ tftpd
41
+ Usage: tftpd [OPTIONS] PORT
42
+ -v, --version Show version and exit
43
+ -d, --debug Enable debug output
44
+ -l, --log PATH Log to file
45
+ -b, --background Fork into background
46
+ -h, --host HOST Bind do host
47
+ -p, --path PATH Serving root directory
48
+
49
+ ## Contributing
50
+
51
+ Fell free to contributed patches using the common GitHub model (descried below). I'm also interested in cases where something doesn't work (weird platforms etc.). If you feel like it please write the client side.
52
+
53
+ - Fork the repo
54
+ - Checkout a new branch for your changes
55
+ - Write code and tests, check, commit
56
+ - Make a Pull Request
57
+
58
+ ## Licensing
59
+
60
+ Standard two-clause BSD license, see LICENSE.txt for details.
61
+
62
+ Copyright (c) 2015 Piotr S. Staszewski
@@ -0,0 +1,37 @@
1
+ require 'rake/testtask'
2
+
3
+ begin
4
+ require 'rubygems/tasks'
5
+ Gem::Tasks.new
6
+ rescue LoadError => e
7
+ warn e.message
8
+ end
9
+
10
+ begin
11
+ require 'yard'
12
+ YARD::Rake::YardocTask.new
13
+ task :doc => :yard
14
+ rescue LoadError => e
15
+ warn e.message
16
+ end
17
+
18
+ task :default => :test
19
+
20
+ Rake::TestTask.new do |t|
21
+ t.libs = ['lib', 'test']
22
+ t.name = 'test'
23
+ t.description = 'Run all tests'
24
+ t.warning = true
25
+ t.test_files = FileList['test/*.rb']
26
+ end
27
+
28
+ FileList['test/*.rb'].each do |p|
29
+ name = p.split('/').last.split('.').first
30
+ Rake::TestTask.new do |t|
31
+ t.libs = ['lib', 'test']
32
+ t.name = "test:#{name}"
33
+ t.description = "Run only #{name} specific tests"
34
+ t.warning = true
35
+ t.test_files = [p]
36
+ end
37
+ end
@@ -0,0 +1,60 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+
4
+ $LOAD_PATH.unshift File.join(File.dirname(__FILE__), '..', 'lib')
5
+
6
+ require 'logger'
7
+ require 'optparse'
8
+ require 'tftp'
9
+
10
+ config = {:path => Dir.pwd, :host => '127.0.0.1', :fork => false,
11
+ :ver => false, :loglevel => Logger::INFO, :logfile => STDOUT}
12
+
13
+ def die!(msg)
14
+ STDERR.puts msg
15
+ exit(2)
16
+ end
17
+
18
+ op = OptionParser.new do |o|
19
+ o.banner = "Usage: #{$PROGRAM_NAME} [OPTIONS] PORT"
20
+ o.on('-v', '--version', 'Show version and exit') { config[:ver] = true }
21
+ o.on('-d', '--debug', 'Enable debug output') { config[:loglevel] = Logger::DEBUG }
22
+ o.on('-l', '--log PATH', String, 'Log to file') {|a| config[:logfile] = a }
23
+ o.on('-b', '--background', 'Fork into background') {|a| config[:fork] = true }
24
+ o.on('-h', '--host HOST', String, 'Bind do host') {|a| config[:host] = a }
25
+ o.on('-p', '--path PATH', String, 'Serving root directory') {|a| config[:path] = a }
26
+ end
27
+ op.parse! or die!(op)
28
+
29
+ if config[:ver]
30
+ puts "fx-tftpd v#{TFTP::VERSION} Copyright (c) 2015, Piotr S. Staszewski"
31
+ exit
32
+ end
33
+
34
+ die!('Serving root does not exists') unless File.exists? config[:path]
35
+
36
+ PORT = ARGV.shift.to_i
37
+ die!(op) if PORT < 1 || PORT > 65535
38
+
39
+ log = Logger.new(config[:logfile])
40
+ log.level = config[:loglevel]
41
+ log.formatter = lambda {|s, d, p, m| "#{d.strftime('%Y-%m-%d %H:%M:%S.%3N')} | #{s.ljust(5)} | #{m}\n" }
42
+
43
+ if config[:fork]
44
+ log.info 'Detaching from the console'
45
+ Process.daemon(true)
46
+ end
47
+
48
+ begin
49
+ log.info "Serving from and to #{config[:path]}"
50
+ srv = TFTP::Server::RWSimple.new(config[:path], :host => config[:host], :port => PORT, :logger => log)
51
+ srv.run!
52
+ rescue SignalException => e
53
+ puts if e.is_a? Interrupt
54
+ srv.stop
55
+ end
56
+
57
+ if Thread.list.length > 1
58
+ log.info 'Waiting for outstanding connections'
59
+ Thread.stop
60
+ end
@@ -0,0 +1,26 @@
1
+ # coding: utf-8
2
+ #
3
+
4
+ require File.expand_path('../lib/tftp/version', __FILE__)
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = 'fx-tftp'
8
+ s.version = TFTP::VERSION
9
+ s.date = Time.now
10
+
11
+ s.summary = %q{Hackable and ACTUALLY WORKING pure-Ruby TFTP server}
12
+ s.description = %q{Got carried away a bit with the OOness of the whole thing, so while it won't be the fastest TFTP server it might be the most flexible, at least for pure-Ruby ones. With all the infastructure already in place adding a client should be a breeze, should anyone need it.}
13
+ s.license = 'BSD'
14
+ s.authors = ['Piotr S. Staszewski']
15
+ s.email = 'p.staszewski@gmail.com'
16
+ s.homepage = 'https://github.com/drbig/fx-tftp'
17
+
18
+ s.files = `git ls-files`.split("\n")
19
+ s.test_files = s.files.grep(%r{^test/})
20
+ s.require_paths = ['lib']
21
+
22
+ s.required_ruby_version = '>= 1.9.3'
23
+
24
+ s.add_development_dependency 'rubygems-tasks', '~> 0.2'
25
+ s.add_development_dependency 'minitest', '~> 5.4'
26
+ end
@@ -0,0 +1,2 @@
1
+ require 'tftp/tftp'
2
+ require 'tftp/version'
@@ -0,0 +1,371 @@
1
+ # After https://www.ietf.org/rfc/rfc1350.txt
2
+ #
3
+
4
+ require 'socket'
5
+
6
+ module TFTP
7
+ # TFTP-specific errors.
8
+ class Error < Exception; end
9
+ # Packet parsing exception.
10
+ class ParseError < Error; end
11
+
12
+ # Packet can parse a binary string into a lightweight object representation.
13
+ module Packet
14
+ # Base is a thin layer over a Struct.
15
+ class Base < Struct
16
+ # Encode the packet back to binary string.
17
+ # It uses the #to_str method to properly format each packet, and then forces
18
+ # 8bit encoding.
19
+ def encode; to_str.force_encoding('ascii-8bit'); end
20
+ end
21
+
22
+ # Read Request
23
+ RRQ = Base.new(:filename, :mode)
24
+ class RRQ
25
+ # Convert to binary string.
26
+ def to_str; "\x00\x01" + self.filename + "\x00" + self.mode.to_s + "\x00"; end
27
+ end
28
+
29
+ # Write Request
30
+ WRQ = Base.new(:filename, :mode)
31
+ class WRQ
32
+ def to_str; "\x00\x02" + self.filename + "\x00" + self.mode.to_s + "\x00"; end
33
+ end
34
+
35
+ # Data
36
+ DATA = Base.new(:seq, :data)
37
+ class DATA
38
+ def to_str; "\x00\x03" + [self.seq].pack('n') + self.data; end
39
+ # Check if this is the last data packet for this session.
40
+ def last?; self.data.length < 512; end
41
+ end
42
+
43
+ # Acknowledgement
44
+ ACK = Base.new(:seq)
45
+ class ACK
46
+ def to_str; "\x00\x04" + [self.seq].pack('n'); end
47
+ end
48
+
49
+ # Error
50
+ ERROR = Base.new(:code, :msg)
51
+ class ERROR
52
+ def to_str; "\x00\x05" + [self.code].pack('n') + self.msg + "\x00"; end
53
+ end
54
+
55
+ # Parse a binary string into a packet.
56
+ # Does some sanity checking, can raise a ParseError.
57
+ def self.parse(data)
58
+ data = data.force_encoding('ascii-8bit')
59
+
60
+ opcode = data.unpack('n').first
61
+ if opcode < 1 || opcode > 5
62
+ raise ParseError, "Unknown packet opcode '#{opcode.inspect}'"
63
+ end
64
+
65
+ payload = data.slice(2, data.length - 2)
66
+ case opcode
67
+ when 1, 2 # rrq, wrq
68
+ raise ParseError, 'Not null terminated' if payload.slice(payload.length - 1) != "\x00"
69
+ xs = payload.split("\x00")
70
+ raise ParseError, "Not enough elements: #{xs.inspect}" if xs.length < 2
71
+ filename = xs[0]
72
+ mode = xs[1].downcase.to_sym
73
+ raise ParseError, "Unknown mode '#{xs[1].inspect}'" unless [:netascii, :octet].member? mode
74
+ return RRQ.new(filename, mode) if opcode == 1
75
+ return WRQ.new(filename, mode)
76
+ when 3 # data
77
+ seq = payload.unpack('n').first
78
+ block = payload.slice(2, payload.length - 2) || ''
79
+ raise ParseError, "Exceeded block length with #{block.length} bytes" if block.length > 512
80
+ return DATA.new(seq, block)
81
+ when 4 # ack
82
+ raise ParseError, "Wrong payload length with #{payload.length} bytes" if payload.length != 2
83
+ seq = payload.unpack('n').first
84
+ return ACK.new(seq)
85
+ when 5 # error
86
+ raise ParseError, 'Not null terminated' if payload.slice(payload.length - 1) != "\x00"
87
+ code = payload.unpack('n').first
88
+ raise ParseError, "Unknown error code '#{code.inspect}'" if code < 0 || code > 7
89
+ msg = payload.slice(2, payload.length - 3) || ''
90
+ return ERROR.new(code, msg)
91
+ end
92
+ end
93
+ end
94
+
95
+ # Handlers implement session-handling logic.
96
+ module Handler
97
+ # Base handler contains the common methods for real handlers.
98
+ class Base
99
+ # Initialize the handler.
100
+ #
101
+ # Options:
102
+ #
103
+ # - :logger => logger object (e.g. a Logger instance)
104
+ # - :timeout => used while waiting for next DATA/ACK packets (default: 5s)
105
+ #
106
+ # All given options are saved in @opts.
107
+ #
108
+ # @param opts [Hash] Options
109
+ def initialize(opts = {})
110
+ @logger = opts[:logger]
111
+ @timeout = opts[:timeout] || 5
112
+ @opts = opts
113
+ end
114
+
115
+ # Send data over an established connection.
116
+ #
117
+ # Doesn't close neither sock nor io.
118
+ #
119
+ # @param tag [String] Tag used for logging
120
+ # @param sock [UDPSocket] Connected socket
121
+ # @param io [IO] Object to send data from
122
+ def send(tag, sock, io)
123
+ seq = 1
124
+ begin
125
+ while not io.eof?
126
+ block = io.read(512)
127
+ sock.send(Packet::DATA.new(seq, block).encode, 0)
128
+ unless IO.select([sock], nil, nil, @timeout)
129
+ log :warn, "#{tag} Timeout at block ##{seq}"
130
+ return
131
+ end
132
+ msg, _ = sock.recvfrom(4, 0)
133
+ pkt = Packet.parse(msg)
134
+ if pkt.class != Packet::ACK
135
+ log :warn, "#{tag} Expected ACK but got: #{pkt.class}"
136
+ return
137
+ end
138
+ if pkt.seq != seq
139
+ log :warn, "#{tag} Seq mismatch: #{seq} != #{pkt.seq}"
140
+ return
141
+ end
142
+ seq += 1
143
+ end
144
+ rescue ParseError => e
145
+ log :warn, "#{tag} Packet parse error: #{e.to_s}"
146
+ return
147
+ end
148
+ log :info, "#{tag} Sent file"
149
+ end
150
+
151
+ # Receive data over an established connection.
152
+ #
153
+ # Doesn't close neither sock nor io.
154
+ # Returns true if whole file has been received, false otherwise.
155
+ #
156
+ # @param tag [String] Tag used for logging
157
+ # @param sock [UDPSocket] Connected socket
158
+ # @param io [IO] Object to write data to
159
+ # @return [Boolean]
160
+ def recv(tag, sock, io)
161
+ sock.send(Packet::ACK.new(0).encode, 0)
162
+ seq = 1
163
+ begin
164
+ loop do
165
+ unless IO.select([sock], nil, nil, @timeout)
166
+ log :warn, "#{tag} Timeout at block ##{seq}"
167
+ return false
168
+ end
169
+ msg, _ = sock.recvfrom(516, 0)
170
+ pkt = Packet.parse(msg)
171
+ if pkt.class != Packet::DATA
172
+ log :warn, "#{tag} Expected DATA but got: #{pkt.class}"
173
+ return false
174
+ end
175
+ if pkt.seq != seq
176
+ log :warn, "#{tag} Seq mismatch: #{seq} != #{pkt.seq}"
177
+ return false
178
+ end
179
+ io.write(pkt.data)
180
+ sock.send(Packet::ACK.new(seq).encode, 0)
181
+ break if pkt.last?
182
+ seq += 1
183
+ end
184
+ rescue ParseError => e
185
+ log :warn, "#{tag} Packet parse error: #{e.to_s}"
186
+ return false
187
+ end
188
+ log :info, "#{tag} Received file"
189
+ true
190
+ end
191
+
192
+ private
193
+ def log(level, msg)
194
+ @logger.send(level, msg) if @logger
195
+ end
196
+ end
197
+
198
+ # Basic read-write session over a 'physical' directory.
199
+ class RWSimple < Base
200
+ # @param path [String] Path to serving root directory
201
+ def initialize(path, opts = {})
202
+ @path = path
203
+ super(opts)
204
+ end
205
+
206
+ # Handle a session.
207
+ #
208
+ # Has to close the socket (and any other resources).
209
+ # Note that the current version 'guards' against path traversal by a simple
210
+ # substitution of '..' with '__'.
211
+ #
212
+ # @param tag [String] Tag used for logging
213
+ # @param req [Packet] The initial request packet
214
+ # @param sock [UDPSocket] Connected socket
215
+ # @param src [UDPSource] Initial connection information
216
+ def run!(tag, req, sock, src)
217
+ name = req.filename.gsub('..', '__')
218
+ path = File.join(@path, name)
219
+
220
+ case req
221
+ when Packet::RRQ
222
+ log :info, "#{tag} Read request for #{req.filename} (#{req.mode})"
223
+ unless File.exist? path
224
+ log :warn, "#{tag} File not found"
225
+ sock.send(Packet::ERROR.new(1, 'File not found.').encode, 0)
226
+ sock.close
227
+ return
228
+ end
229
+ mode = 'r'
230
+ mode += 'b' if req.mode == :octet
231
+ io = File.open(path, mode)
232
+ send(tag, sock, io)
233
+ sock.close
234
+ io.close
235
+ when Packet::WRQ
236
+ log :info, "#{tag} Write request for #{req.filename} (#{req.mode})"
237
+ if File.exist? path
238
+ log :warn, "#{tag} File already exist"
239
+ sock.send(Packet::ERROR.new(6, 'File already exists.').encode, 0)
240
+ sock.close
241
+ return
242
+ end
243
+ mode = 'w'
244
+ mode += 'b' if req.mode == :octet
245
+ io = File.open(path, mode)
246
+ ok = recv(tag, sock, io)
247
+ sock.close
248
+ io.close
249
+ unless ok
250
+ log :warn, "#{tag} Removing partial file #{req.filename}"
251
+ File.delete(path)
252
+ end
253
+ end
254
+ end
255
+ end
256
+ end
257
+
258
+ # Servers customize the Basic server and perhaps combine it with a handler.
259
+ module Server
260
+ # Basic server utilizing threads for handling sessions.
261
+ #
262
+ # It lacks a mutex around access to @clients, in case you'd want to stress
263
+ # test it for 10K or something.
264
+ #
265
+ # @attr handler [Handler] Session handler
266
+ # @attr host [String] Host the sockets bind to
267
+ # @attr port [Integer] Session dispatcher port
268
+ # @attr clients [Hash] Current sessions
269
+ class Base
270
+ attr_reader :handler, :host, :port, :clients
271
+
272
+ # Initialize the server.
273
+ #
274
+ # Options:
275
+ #
276
+ # - :host => host to bind to (default: 127.0.0.1)
277
+ # - :port => dispatcher port (default: 69)
278
+ # - :logger => logger instance
279
+ #
280
+ # @param handler [Handler] Initialized session handler
281
+ # @param opts [Hash] Options
282
+ def initialize(handler, opts = {})
283
+ @handler = handler
284
+
285
+ @host = opts[:host] || '127.0.0.1'
286
+ @port = opts[:port] || 69
287
+ @logger = opts[:logger]
288
+
289
+ @clients = Hash.new
290
+ @run = false
291
+ end
292
+
293
+ # Run the main server loop.
294
+ #
295
+ # This is obviously blocking.
296
+ def run!
297
+ log :info, "UDP server loop at #{@host}:#{@port}"
298
+ @run = true
299
+ Socket.udp_server_loop(@host, @port) do |msg, src|
300
+ break unless @run
301
+
302
+ addr = src.remote_address
303
+ tag = "[#{addr.ip_address}:#{addr.ip_port.to_s.ljust(5)}]"
304
+ log :info, "#{tag} New initial packet received"
305
+
306
+ begin
307
+ pkt = Packet.parse(msg)
308
+ rescue ParseError => e
309
+ log :warn, "#{tag} Packet parse error: #{e.to_s}"
310
+ next
311
+ end
312
+
313
+ log :debug, "#{tag} -> PKT: #{pkt.inspect}"
314
+ tid = get_tid
315
+ tag = "[#{addr.ip_address}:#{addr.ip_port.to_s.ljust(5)}:#{tid.to_s.ljust(5)}]"
316
+ sock = addr.connect_from(@host, tid)
317
+ @clients[tid] = tag
318
+
319
+ unless pkt.is_a?(Packet::RRQ) || pkt.is_a?(Packet::WRQ)
320
+ log :warn, "#{tag} Bad initial packet: #{pkt.class}"
321
+ sock.send(Packet::ERROR.new(4, 'Illegal TFTP operation.').encode, 0)
322
+ sock.close
323
+ next
324
+ end
325
+
326
+ Thread.new do
327
+ @handler.run!(tag, pkt, sock, src)
328
+ @clients.delete(tid)
329
+ log :info, "#{tag} Session ended"
330
+ end
331
+ end
332
+ log :info, 'UDP server loop has stopped'
333
+ end
334
+
335
+ # Stop the main server loop.
336
+ #
337
+ # This will allow the currently pending sessions to finish.
338
+ def stop
339
+ log :info, 'Stopping UDP server loop'
340
+ @run = false
341
+ UDPSocket.new.send('break', 0, @host, @port)
342
+ end
343
+
344
+ private
345
+ # Get the server's TID.
346
+ #
347
+ # The TID is basically a random port number we will use for a session.
348
+ # This actually tries to get a unique TID per session.
349
+ # It uses only ports 1024 - 65535 as not to require root.
350
+ def get_tid
351
+ tid = 1024 + rand(64512)
352
+ tid = 1024 + rand(64512) while @clients.has_key? tid
353
+ tid
354
+ end
355
+
356
+ def log(level, msg)
357
+ @logger.send(level, msg) if @logger
358
+ end
359
+ end
360
+
361
+ # Basic read-write TFTP server.
362
+ #
363
+ # This is what most other TFTPd implementations give you.
364
+ class RWSimple < Base
365
+ def initialize(path, opts = {})
366
+ handler = Handler::RWSimple.new(path, opts)
367
+ super(handler, opts)
368
+ end
369
+ end
370
+ end
371
+ end
@@ -0,0 +1,4 @@
1
+ module TFTP
2
+ # Current version string.
3
+ VERSION = '0.3'
4
+ end
@@ -0,0 +1,110 @@
1
+ $LOAD_PATH.unshift File.join(File.dirname(__FILE__), '..', 'lib')
2
+
3
+ require 'minitest/autorun'
4
+ require 'tftp'
5
+
6
+ class Packet < Minitest::Test
7
+ def test_parse_rrq
8
+ assert_equal TFTP::Packet::RRQ.new('test.txt', :netascii),
9
+ TFTP::Packet.parse("\x00\x01test.txt\x00netascii\x00")
10
+ assert_equal TFTP::Packet::RRQ.new('binary', :octet),
11
+ TFTP::Packet.parse("\x00\x01binary\x00octet\x00")
12
+ assert_equal TFTP::Packet::RRQ.new('test.txt', :netascii),
13
+ TFTP::Packet.parse("\x00\x01test.txt\x00nEtasCIi\x00")
14
+ assert_equal TFTP::Packet::RRQ.new('binary.exe', :octet),
15
+ TFTP::Packet.parse("\x00\x01binary.exe\x00OCTET\x00")
16
+
17
+
18
+ assert_raises(TFTP::ParseError) { TFTP::Packet.parse("\x00\x01\x00\x00") }
19
+ assert_raises(TFTP::ParseError) { TFTP::Packet.parse("\x00\x01\x00\x00\x00") }
20
+ assert_raises(TFTP::ParseError) { TFTP::Packet.parse("\x00\x01a\x00c\x00c\x00") }
21
+ assert_raises(TFTP::ParseError) { TFTP::Packet.parse("\x00\x01foo\x00bar\x00") }
22
+ end
23
+
24
+ def test_encode_rrq
25
+ assert_equal "\x00\x01test.txt\x00netascii\x00",
26
+ TFTP::Packet::RRQ.new('test.txt', :netascii).encode
27
+ assert_equal "\x00\x01binary\x00octet\x00",
28
+ TFTP::Packet::RRQ.new('binary', :octet).encode
29
+ end
30
+
31
+ def test_parse_wrq
32
+ assert_equal TFTP::Packet::WRQ.new('test.txt', :netascii),
33
+ TFTP::Packet.parse("\x00\x02test.txt\x00netascii\x00")
34
+ assert_equal TFTP::Packet::WRQ.new('binary', :octet),
35
+ TFTP::Packet.parse("\x00\x02binary\x00octet\x00")
36
+ assert_equal TFTP::Packet::WRQ.new('test.txt', :netascii),
37
+ TFTP::Packet.parse("\x00\x02test.txt\x00NetascIi\x00")
38
+ assert_equal TFTP::Packet::WRQ.new('binary', :octet),
39
+ TFTP::Packet.parse("\x00\x02binary\x00OctEt\x00")
40
+
41
+
42
+ assert_raises(TFTP::ParseError) { TFTP::Packet.parse("\x00\x02\x00\x00\x00") }
43
+ assert_raises(TFTP::ParseError) { TFTP::Packet.parse("\x00\x02a\x00c\x00c\x00") }
44
+ assert_raises(TFTP::ParseError) { TFTP::Packet.parse("\x00\x02foo\x00bar\x00") }
45
+ end
46
+
47
+ def test_encode_wrq
48
+ assert_equal "\x00\x02test.txt\x00netascii\x00",
49
+ TFTP::Packet::WRQ.new('test.txt', :netascii).encode
50
+ assert_equal "\x00\x02binary\x00octet\x00",
51
+ TFTP::Packet::WRQ.new('binary', :octet).encode
52
+ end
53
+
54
+ def test_parse_data
55
+ assert_equal TFTP::Packet::DATA.new(0, "1234"),
56
+ TFTP::Packet.parse("\x00\x03\x00\x001234")
57
+ assert_equal TFTP::Packet::DATA.new(16, ('a' * 512)),
58
+ TFTP::Packet.parse("\x00\x03\x00\x10" + ('a' * 512))
59
+ assert TFTP::Packet.parse("\x00\x03\x00\x001234").last?
60
+ assert_equal TFTP::Packet::DATA.new(16, ''),
61
+ TFTP::Packet.parse("\x00\x03\x00\x10")
62
+ assert TFTP::Packet.parse("\x00\x03\x00\x10").last?
63
+
64
+ assert_raises(TFTP::ParseError) { TFTP::Packet.parse("\x00\x03\x00\x00" + ('a' * 513)) }
65
+ end
66
+
67
+ def test_encode_data
68
+ assert_equal "\x00\x03\x00\x001234",
69
+ TFTP::Packet::DATA.new(0, "1234").encode
70
+ assert_equal "\x00\x03\x00\x10" + ('a' * 512),
71
+ TFTP::Packet::DATA.new(16, ('a' * 512)).encode
72
+ end
73
+
74
+ def test_parse_ack
75
+ assert_equal TFTP::Packet::ACK.new(0),
76
+ TFTP::Packet.parse("\x00\x04\x00\x00")
77
+ assert_equal TFTP::Packet::ACK.new(64434),
78
+ TFTP::Packet.parse("\x00\x04\xfb\xb2")
79
+
80
+
81
+ assert_raises(TFTP::ParseError) { TFTP::Packet.parse("\x00\x04\x00") }
82
+ assert_raises(TFTP::ParseError) { TFTP::Packet.parse("\x00\x04\x00" + ('A' * 8)) }
83
+ end
84
+
85
+ def test_encode_ack
86
+ assert_equal "\x00\x04\x00\x00",
87
+ TFTP::Packet::ACK.new(0).encode
88
+ assert_equal "\x00\x04\x00\x01",
89
+ TFTP::Packet::ACK.new(1).encode
90
+ end
91
+
92
+ def test_parse_error
93
+ assert_equal TFTP::Packet::ERROR.new(0, 'Not defined, see error message (if any).'),
94
+ TFTP::Packet.parse("\x00\x05\x00\x00Not defined, see error message (if any).\x00")
95
+ assert_equal TFTP::Packet::ERROR.new(7, 'No such user.'),
96
+ TFTP::Packet.parse("\x00\x05\x00\x07No such user.\x00")
97
+ assert_equal TFTP::Packet::ERROR.new(3, ''),
98
+ TFTP::Packet.parse("\x00\x05\x00\x03\x00")
99
+
100
+ assert_raises(TFTP::ParseError) { TFTP::Packet.parse("\x00\x05\x00\xff\x00") }
101
+ assert_raises(TFTP::ParseError) { TFTP::Packet.parse("\x00\x05\x00\x03") }
102
+ end
103
+
104
+ def test_encode_error
105
+ assert_equal "\x00\x05\x00\x07No such user.\x00",
106
+ TFTP::Packet::ERROR.new(7, 'No such user.').encode
107
+ assert_equal "\x00\x05\x00\x03\x00",
108
+ TFTP::Packet::ERROR.new(3, '').encode
109
+ end
110
+ end
metadata ADDED
@@ -0,0 +1,88 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: fx-tftp
3
+ version: !ruby/object:Gem::Version
4
+ version: '0.3'
5
+ platform: ruby
6
+ authors:
7
+ - Piotr S. Staszewski
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-01-23 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rubygems-tasks
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '0.2'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '0.2'
27
+ - !ruby/object:Gem::Dependency
28
+ name: minitest
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '5.4'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '5.4'
41
+ description: Got carried away a bit with the OOness of the whole thing, so while it
42
+ won't be the fastest TFTP server it might be the most flexible, at least for pure-Ruby
43
+ ones. With all the infastructure already in place adding a client should be a breeze,
44
+ should anyone need it.
45
+ email: p.staszewski@gmail.com
46
+ executables: []
47
+ extensions: []
48
+ extra_rdoc_files: []
49
+ files:
50
+ - ".document"
51
+ - ".gitignore"
52
+ - ".travis.yml"
53
+ - ".yardopts"
54
+ - LICENSE.txt
55
+ - README.md
56
+ - Rakefile
57
+ - bin/tftpd
58
+ - fx-tftp.gemspec
59
+ - lib/tftp.rb
60
+ - lib/tftp/tftp.rb
61
+ - lib/tftp/version.rb
62
+ - test/packet.rb
63
+ homepage: https://github.com/drbig/fx-tftp
64
+ licenses:
65
+ - BSD
66
+ metadata: {}
67
+ post_install_message:
68
+ rdoc_options: []
69
+ require_paths:
70
+ - lib
71
+ required_ruby_version: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: 1.9.3
76
+ required_rubygems_version: !ruby/object:Gem::Requirement
77
+ requirements:
78
+ - - ">="
79
+ - !ruby/object:Gem::Version
80
+ version: '0'
81
+ requirements: []
82
+ rubyforge_project:
83
+ rubygems_version: 2.4.5
84
+ signing_key:
85
+ specification_version: 4
86
+ summary: Hackable and ACTUALLY WORKING pure-Ruby TFTP server
87
+ test_files:
88
+ - test/packet.rb