netconfgen 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/Gemfile +4 -0
- data/Rakefile +35 -0
- data/bin/confgen +99 -0
- data/lib/netconfgen/netconfgen.rb +469 -0
- data/lib/netconfgen/version.rb +4 -0
- data/lib/netconfgen.rb +1 -0
- data/netconfgen.gemspec +21 -0
- data/netconfgen.yaml +4 -0
- data/test/blockreader.rb +37 -0
- data/test/data/file1.txt +6 -0
- data/test/data/file2.txt +9 -0
- data/test/data/variables.txt +4 -0
- data/test/data.json +10 -0
- metadata +104 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: e74877df80cf028688b27e0712cc3d56b15caa44
|
4
|
+
data.tar.gz: 37c95750d391b9f41217cd724f1e08e8e21f5184
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 12e26b6ae98233cca7b0534c3affcf99405410c02adb99cf60ea167feba85865e6d294c182709e036ccdd79c342688f2fe4ec19f2b9a84c65c29310eef39616c
|
7
|
+
data.tar.gz: e334f1c2070bde07d19a111362fc19ceb87a9778c63da993524ed5d2d5d5cebdbf8d5d8f304ddbed051f155203250b34893c52958895c9ea0c4585601535a8b1
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/Rakefile
ADDED
@@ -0,0 +1,35 @@
|
|
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.warning = true
|
24
|
+
t.test_files = FileList['test/*.rb']
|
25
|
+
end
|
26
|
+
|
27
|
+
FileList['test/*.rb'].each do |p|
|
28
|
+
name = p.split('/').last.split('.').first
|
29
|
+
Rake::TestTask.new do |t|
|
30
|
+
t.libs = ['lib', 'test']
|
31
|
+
t.name = "test:#{name}"
|
32
|
+
t.warning = true
|
33
|
+
t.test_files = [p]
|
34
|
+
end
|
35
|
+
end
|
data/bin/confgen
ADDED
@@ -0,0 +1,99 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
$LOAD_PATH.unshift File.join(File.dirname(__FILE__), '..', 'lib')
|
4
|
+
|
5
|
+
require 'logger'
|
6
|
+
require 'optparse'
|
7
|
+
require 'netconfgen'
|
8
|
+
require 'pp'
|
9
|
+
require 'yaml'
|
10
|
+
require 'json'
|
11
|
+
|
12
|
+
CONFIG_FILE = ["/etc/netconfgen.yaml", "~/.netconfgen.yaml", "netconfgen.yaml"]
|
13
|
+
config = {}
|
14
|
+
CONFIG_FILE.each do |fname|
|
15
|
+
fname = File.expand_path(fname)
|
16
|
+
if File.exists?(fname)
|
17
|
+
config = YAML::load_file(fname)
|
18
|
+
config = config.each_with_object({}) { |(k,v),memo| memo[k.to_sym] = v }
|
19
|
+
break
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
config[:variables] ||= {}
|
24
|
+
config[:root] ||= 'test/data'
|
25
|
+
|
26
|
+
# Defaults
|
27
|
+
op = OptionParser.new do |o|
|
28
|
+
o.banner = "Usage: #{$PROGRAM_NAME} [OPTIONS] MAIN_BLOCK_NAME"
|
29
|
+
o.on('-l', '--listen', 'Run a listening server instead') do
|
30
|
+
config[:listen] = true
|
31
|
+
end
|
32
|
+
o.on('-o [OPTION]', 'Pass a template variable') do |arg|
|
33
|
+
if m = arg.match(/(.+?)=(.+)/)
|
34
|
+
config[:variables][m[1]] = m[2]
|
35
|
+
end
|
36
|
+
end
|
37
|
+
o.on('--json=[VARIABLE=FILE]', 'Load json into a variable') do |arg|
|
38
|
+
if m = arg.match(/(.+?)=(.+)/)
|
39
|
+
data = File.read(m[2])
|
40
|
+
config[:variables][m[1]] = JSON.parse(data)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
o.on('-a', '--address ADDRESS', String,
|
44
|
+
'Address to listen on (default: 0.0.0.0)') do |address|
|
45
|
+
config[:address] = address
|
46
|
+
end
|
47
|
+
o.on('-h', '--http PORT', Integer,
|
48
|
+
'Port to listen on for http (default: 8080)') do |port|
|
49
|
+
abort 'Invalid port' if port < 1 || port > 65535
|
50
|
+
config[:http] = http
|
51
|
+
end
|
52
|
+
o.on('-v', '--verbose', 'Enable verbose output') do
|
53
|
+
config[:verbose] = true
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
config[:name] = op.parse!.first
|
58
|
+
abort 'Last argument must be the main block name' unless config[:name]
|
59
|
+
|
60
|
+
if config[:listen] && config[:port] < 1024 && !Process.euid.zero?
|
61
|
+
abort 'Please run tftpd as root via sudo!'
|
62
|
+
end
|
63
|
+
|
64
|
+
log = Logger.new(STDOUT)
|
65
|
+
log.level = config[:verbose] ? "DEBUG" : "INFO"
|
66
|
+
log.formatter = lambda do |s, d, p, m|
|
67
|
+
"#{d.strftime('%Y-%m-%d %H:%M:%S.%3N')} | #{s.ljust(5)} | #{m}\n"
|
68
|
+
end
|
69
|
+
config[:logger] = log
|
70
|
+
|
71
|
+
if config[:verbose]
|
72
|
+
PP.pp(config, STDERR)
|
73
|
+
end
|
74
|
+
|
75
|
+
if !config[:listen]
|
76
|
+
STDERR.puts "Rendering block #{config[:name]}" if config[:verbose]
|
77
|
+
br = NetConfGen::BlockEngine.new(config[:root])
|
78
|
+
|
79
|
+
config[:variables].each do |k, v|
|
80
|
+
br.set(k, v)
|
81
|
+
end
|
82
|
+
block = br.load(config[:name])
|
83
|
+
puts block.render
|
84
|
+
exit(0)
|
85
|
+
end
|
86
|
+
|
87
|
+
begin
|
88
|
+
log.info "Serving from and to #{config[:path]}"
|
89
|
+
srv = TFTP::Server::RWSimple.new(config[:path], config)
|
90
|
+
srv.run!
|
91
|
+
rescue SignalException => e
|
92
|
+
puts if e.is_a? Interrupt
|
93
|
+
srv.stop
|
94
|
+
end
|
95
|
+
|
96
|
+
if Thread.list.length > 1
|
97
|
+
log.info 'Waiting for outstanding connections'
|
98
|
+
Thread.stop
|
99
|
+
end
|
@@ -0,0 +1,469 @@
|
|
1
|
+
# After https://www.ietf.org/rfc/rfc1350.txt
|
2
|
+
#
|
3
|
+
|
4
|
+
require 'erb'
|
5
|
+
|
6
|
+
|
7
|
+
module NetConfGen
|
8
|
+
|
9
|
+
|
10
|
+
class Block
|
11
|
+
attr_accessor :code, :blockengine
|
12
|
+
|
13
|
+
def initialize(name)
|
14
|
+
@name = name
|
15
|
+
end
|
16
|
+
|
17
|
+
def render
|
18
|
+
t = ERB.new(@code)
|
19
|
+
str = t.result(@blockengine.context.instance_eval { binding })
|
20
|
+
return str
|
21
|
+
end
|
22
|
+
|
23
|
+
def to_s
|
24
|
+
self.render
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
|
29
|
+
class BlockContext
|
30
|
+
def initialize(blockengine)
|
31
|
+
@blockengine = blockengine
|
32
|
+
end
|
33
|
+
|
34
|
+
def include(name)
|
35
|
+
block = @blockengine.load(name)
|
36
|
+
return block.render
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
class BlockEngine
|
41
|
+
attr_reader :context
|
42
|
+
def initialize(basepath)
|
43
|
+
@basepath = basepath
|
44
|
+
if !File.directory?(basepath)
|
45
|
+
raise "Basepath #{basepath} does not exists"
|
46
|
+
end
|
47
|
+
@suffix = '.txt'
|
48
|
+
|
49
|
+
@blocks = {}
|
50
|
+
|
51
|
+
@context = BlockContext.new(self)
|
52
|
+
end
|
53
|
+
|
54
|
+
def set(key, value)
|
55
|
+
@context.instance_variable_set("@" + key, value)
|
56
|
+
end
|
57
|
+
|
58
|
+
def load(name)
|
59
|
+
if @blocks[name]
|
60
|
+
return @blocks[name]
|
61
|
+
end
|
62
|
+
|
63
|
+
@code = '';
|
64
|
+
File.open(@basepath + '/' + name + @suffix, "r") do |f|
|
65
|
+
code_started = false
|
66
|
+
code = ''
|
67
|
+
f.each_line do |line|
|
68
|
+
if line == "<code>\n"
|
69
|
+
code_started = true
|
70
|
+
elsif line == "</code>\n" || line == "</code>"
|
71
|
+
code_started = false
|
72
|
+
elsif code_started == true
|
73
|
+
code += line
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
block = Block.new(name)
|
78
|
+
block.code = code
|
79
|
+
block.blockengine = self
|
80
|
+
@blocks[name] = block
|
81
|
+
|
82
|
+
return block
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
|
88
|
+
# TFTP-specific errors.
|
89
|
+
class Error < Exception; end
|
90
|
+
# Packet parsing exception.
|
91
|
+
class ParseError < Error; end
|
92
|
+
|
93
|
+
# Packet can parse a binary string into a lightweight object representation.
|
94
|
+
module Packet
|
95
|
+
# Base is a thin layer over a Struct.
|
96
|
+
class Base < Struct
|
97
|
+
# Encode the packet back to binary string.
|
98
|
+
# It uses the #to_str method to properly format each packet, and then forces
|
99
|
+
# 8bit encoding.
|
100
|
+
def encode; to_str.force_encoding('ascii-8bit'); end
|
101
|
+
end
|
102
|
+
|
103
|
+
# Read Request
|
104
|
+
RRQ = Base.new(:filename, :mode)
|
105
|
+
class RRQ
|
106
|
+
# Convert to binary string.
|
107
|
+
def to_str; "\x00\x01" + self.filename + "\x00" + self.mode.to_s + "\x00"; end
|
108
|
+
end
|
109
|
+
|
110
|
+
# Write Request
|
111
|
+
WRQ = Base.new(:filename, :mode)
|
112
|
+
class WRQ
|
113
|
+
def to_str; "\x00\x02" + self.filename + "\x00" + self.mode.to_s + "\x00"; end
|
114
|
+
end
|
115
|
+
|
116
|
+
# Data
|
117
|
+
DATA = Base.new(:seq, :data)
|
118
|
+
class DATA
|
119
|
+
def to_str; "\x00\x03" + [self.seq].pack('n') + self.data; end
|
120
|
+
# Check if this is the last data packet for this session.
|
121
|
+
def last?; self.data.length < 512; end
|
122
|
+
end
|
123
|
+
|
124
|
+
# Acknowledgement
|
125
|
+
ACK = Base.new(:seq)
|
126
|
+
class ACK
|
127
|
+
def to_str; "\x00\x04" + [self.seq].pack('n'); end
|
128
|
+
end
|
129
|
+
|
130
|
+
# Error
|
131
|
+
ERROR = Base.new(:code, :msg)
|
132
|
+
class ERROR
|
133
|
+
def to_str; "\x00\x05" + [self.code].pack('n') + self.msg + "\x00"; end
|
134
|
+
end
|
135
|
+
|
136
|
+
# Parse a binary string into a packet.
|
137
|
+
# Does some sanity checking, can raise a ParseError.
|
138
|
+
def self.parse(data)
|
139
|
+
data = data.force_encoding('ascii-8bit')
|
140
|
+
|
141
|
+
opcode = data.unpack('n').first
|
142
|
+
if opcode < 1 || opcode > 5
|
143
|
+
raise ParseError, "Unknown packet opcode '#{opcode.inspect}'"
|
144
|
+
end
|
145
|
+
|
146
|
+
payload = data.slice(2, data.length - 2)
|
147
|
+
case opcode
|
148
|
+
when 1, 2 # rrq, wrq
|
149
|
+
raise ParseError, 'Not null terminated' if payload.slice(payload.length - 1) != "\x00"
|
150
|
+
xs = payload.split("\x00")
|
151
|
+
raise ParseError, "Not enough elements: #{xs.inspect}" if xs.length < 2
|
152
|
+
filename = xs[0]
|
153
|
+
mode = xs[1].downcase.to_sym
|
154
|
+
raise ParseError, "Unknown mode '#{xs[1].inspect}'" unless [:netascii, :octet].member? mode
|
155
|
+
return RRQ.new(filename, mode) if opcode == 1
|
156
|
+
return WRQ.new(filename, mode)
|
157
|
+
when 3 # data
|
158
|
+
seq = payload.unpack('n').first
|
159
|
+
block = payload.slice(2, payload.length - 2) || ''
|
160
|
+
raise ParseError, "Exceeded block length with #{block.length} bytes" if block.length > 512
|
161
|
+
return DATA.new(seq, block)
|
162
|
+
when 4 # ack
|
163
|
+
raise ParseError, "Wrong payload length with #{payload.length} bytes" if payload.length != 2
|
164
|
+
seq = payload.unpack('n').first
|
165
|
+
return ACK.new(seq)
|
166
|
+
when 5 # error
|
167
|
+
raise ParseError, 'Not null terminated' if payload.slice(payload.length - 1) != "\x00"
|
168
|
+
code = payload.unpack('n').first
|
169
|
+
raise ParseError, "Unknown error code '#{code.inspect}'" if code < 0 || code > 7
|
170
|
+
msg = payload.slice(2, payload.length - 3) || ''
|
171
|
+
return ERROR.new(code, msg)
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
# Handlers implement session-handling logic.
|
177
|
+
module Handler
|
178
|
+
# Base handler contains the common methods for real handlers.
|
179
|
+
class Base
|
180
|
+
# Initialize the handler.
|
181
|
+
#
|
182
|
+
# Options:
|
183
|
+
#
|
184
|
+
# - :logger => logger object (e.g. a Logger instance)
|
185
|
+
# - :timeout => used while waiting for next DATA/ACK packets (default: 5s)
|
186
|
+
#
|
187
|
+
# All given options are saved in @opts.
|
188
|
+
#
|
189
|
+
# @param opts [Hash] Options
|
190
|
+
def initialize(opts = {})
|
191
|
+
@logger = opts[:logger]
|
192
|
+
@timeout = opts[:timeout] || 5
|
193
|
+
@opts = opts
|
194
|
+
end
|
195
|
+
|
196
|
+
# Send data over an established connection.
|
197
|
+
#
|
198
|
+
# Doesn't close neither sock nor io.
|
199
|
+
#
|
200
|
+
# @param tag [String] Tag used for logging
|
201
|
+
# @param sock [UDPSocket] Connected socket
|
202
|
+
# @param io [IO] Object to send data from
|
203
|
+
def send(tag, sock, io)
|
204
|
+
seq = 1
|
205
|
+
begin
|
206
|
+
while not io.eof?
|
207
|
+
block = io.read(512)
|
208
|
+
sock.send(Packet::DATA.new(seq, block).encode, 0)
|
209
|
+
unless IO.select([sock], nil, nil, @timeout)
|
210
|
+
log :warn, "#{tag} Timeout at block ##{seq}"
|
211
|
+
return
|
212
|
+
end
|
213
|
+
msg, _ = sock.recvfrom(4, 0)
|
214
|
+
pkt = Packet.parse(msg)
|
215
|
+
if pkt.class != Packet::ACK
|
216
|
+
log :warn, "#{tag} Expected ACK but got: #{pkt.class}"
|
217
|
+
return
|
218
|
+
end
|
219
|
+
if pkt.seq != seq
|
220
|
+
log :warn, "#{tag} Seq mismatch: #{seq} != #{pkt.seq}"
|
221
|
+
return
|
222
|
+
end
|
223
|
+
# Increment with wrap around at 16 bit boundary,
|
224
|
+
# because of tftp block number field size limit.
|
225
|
+
seq = (seq + 1) & 0xFFFF
|
226
|
+
end
|
227
|
+
sock.send(Packet::DATA.new(seq, '').encode, 0) if io.size % 512 == 0
|
228
|
+
rescue ParseError => e
|
229
|
+
log :warn, "#{tag} Packet parse error: #{e.to_s}"
|
230
|
+
return
|
231
|
+
end
|
232
|
+
log :info, "#{tag} Sent file"
|
233
|
+
end
|
234
|
+
|
235
|
+
# Receive data over an established connection.
|
236
|
+
#
|
237
|
+
# Doesn't close neither sock nor io.
|
238
|
+
# Returns true if whole file has been received, false otherwise.
|
239
|
+
#
|
240
|
+
# @param tag [String] Tag used for logging
|
241
|
+
# @param sock [UDPSocket] Connected socket
|
242
|
+
# @param io [IO] Object to write data to
|
243
|
+
# @return [Boolean]
|
244
|
+
def recv(tag, sock, io)
|
245
|
+
sock.send(Packet::ACK.new(0).encode, 0)
|
246
|
+
seq = 1
|
247
|
+
begin
|
248
|
+
loop do
|
249
|
+
unless IO.select([sock], nil, nil, @timeout)
|
250
|
+
log :warn, "#{tag} Timeout at block ##{seq}"
|
251
|
+
return false
|
252
|
+
end
|
253
|
+
msg, _ = sock.recvfrom(516, 0)
|
254
|
+
pkt = Packet.parse(msg)
|
255
|
+
if pkt.class != Packet::DATA
|
256
|
+
log :warn, "#{tag} Expected DATA but got: #{pkt.class}"
|
257
|
+
return false
|
258
|
+
end
|
259
|
+
if pkt.seq != seq
|
260
|
+
log :warn, "#{tag} Seq mismatch: #{seq} != #{pkt.seq}"
|
261
|
+
return false
|
262
|
+
end
|
263
|
+
io.write(pkt.data)
|
264
|
+
sock.send(Packet::ACK.new(seq).encode, 0)
|
265
|
+
break if pkt.last?
|
266
|
+
seq = (seq + 1) & 0xFFFF
|
267
|
+
end
|
268
|
+
rescue ParseError => e
|
269
|
+
log :warn, "#{tag} Packet parse error: #{e.to_s}"
|
270
|
+
return false
|
271
|
+
end
|
272
|
+
log :info, "#{tag} Received file"
|
273
|
+
true
|
274
|
+
end
|
275
|
+
|
276
|
+
private
|
277
|
+
def log(level, msg)
|
278
|
+
@logger.send(level, msg) if @logger
|
279
|
+
end
|
280
|
+
end
|
281
|
+
|
282
|
+
# Basic read-write session over a 'physical' directory.
|
283
|
+
class RWSimple < Base
|
284
|
+
# Initialize the handler.
|
285
|
+
#
|
286
|
+
# Options:
|
287
|
+
#
|
288
|
+
# - :no_read => deny read access if true
|
289
|
+
# - :no_write => deny write access if true
|
290
|
+
#
|
291
|
+
# @param path [String] Path to serving root directory
|
292
|
+
# @param opts [Hash] Options
|
293
|
+
def initialize(path, opts = {})
|
294
|
+
@path = path
|
295
|
+
super(opts)
|
296
|
+
end
|
297
|
+
|
298
|
+
# Handle a session.
|
299
|
+
#
|
300
|
+
# Has to close the socket (and any other resources).
|
301
|
+
# Note that the current version 'guards' against path traversal by a simple
|
302
|
+
# substitution of '..' with '__'.
|
303
|
+
#
|
304
|
+
# @param tag [String] Tag used for logging
|
305
|
+
# @param req [Packet] The initial request packet
|
306
|
+
# @param sock [UDPSocket] Connected socket
|
307
|
+
# @param src [UDPSource] Initial connection information
|
308
|
+
def run!(tag, req, sock, src)
|
309
|
+
name = req.filename.gsub('..', '__')
|
310
|
+
path = File.join(@path, name)
|
311
|
+
|
312
|
+
case req
|
313
|
+
when Packet::RRQ
|
314
|
+
if @opts[:no_read]
|
315
|
+
log :info, "#{tag} Denied read request for #{req.filename}"
|
316
|
+
sock.send(Packet::ERROR.new(2, 'Access denied.').encode, 0)
|
317
|
+
sock.close
|
318
|
+
return
|
319
|
+
end
|
320
|
+
log :info, "#{tag} Read request for #{req.filename} (#{req.mode})"
|
321
|
+
unless File.exist? path
|
322
|
+
log :warn, "#{tag} File not found"
|
323
|
+
sock.send(Packet::ERROR.new(1, 'File not found.').encode, 0)
|
324
|
+
sock.close
|
325
|
+
return
|
326
|
+
end
|
327
|
+
mode = 'r'
|
328
|
+
mode += 'b' if req.mode == :octet
|
329
|
+
io = File.open(path, mode)
|
330
|
+
send(tag, sock, io)
|
331
|
+
sock.close
|
332
|
+
io.close
|
333
|
+
when Packet::WRQ
|
334
|
+
if @opts[:no_write]
|
335
|
+
log :info, "#{tag} Denied write request for #{req.filename}"
|
336
|
+
sock.send(Packet::ERROR.new(2, 'Access denied.').encode, 0)
|
337
|
+
sock.close
|
338
|
+
return
|
339
|
+
end
|
340
|
+
log :info, "#{tag} Write request for #{req.filename} (#{req.mode})"
|
341
|
+
mode = 'w'
|
342
|
+
mode += 'b' if req.mode == :octet
|
343
|
+
io = File.open(path, mode)
|
344
|
+
ok = recv(tag, sock, io)
|
345
|
+
sock.close
|
346
|
+
io.close
|
347
|
+
unless ok
|
348
|
+
log :warn, "#{tag} Removing partial file #{req.filename}"
|
349
|
+
File.delete(path)
|
350
|
+
end
|
351
|
+
end
|
352
|
+
end
|
353
|
+
end
|
354
|
+
end
|
355
|
+
|
356
|
+
# Servers customize the Basic server and perhaps combine it with a handler.
|
357
|
+
module Server
|
358
|
+
# Basic server utilizing threads for handling sessions.
|
359
|
+
#
|
360
|
+
# It lacks a mutex around access to @clients, in case you'd want to stress
|
361
|
+
# test it for 10K or something.
|
362
|
+
#
|
363
|
+
# @attr handler [Handler] Session handler
|
364
|
+
# @attr address [String] Address to listen to
|
365
|
+
# @attr port [Integer] Session dispatcher port
|
366
|
+
# @attr clients [Hash] Current sessions
|
367
|
+
class Base
|
368
|
+
attr_reader :handler, :address, :port, :clients
|
369
|
+
|
370
|
+
# Initialize the server.
|
371
|
+
#
|
372
|
+
# Options:
|
373
|
+
#
|
374
|
+
# - :address => address to listen to (default: '0.0.0.0')
|
375
|
+
# - :port => dispatcher port (default: 69)
|
376
|
+
# - :logger => logger instance
|
377
|
+
#
|
378
|
+
# @param handler [Handler] Initialized session handler
|
379
|
+
# @param opts [Hash] Options
|
380
|
+
def initialize(handler, opts = {})
|
381
|
+
@handler = handler
|
382
|
+
|
383
|
+
@address = opts[:address] || '0.0.0.0'
|
384
|
+
@port = opts[:port] || 69
|
385
|
+
@logger = opts[:logger]
|
386
|
+
|
387
|
+
@clients = Hash.new
|
388
|
+
@run = false
|
389
|
+
end
|
390
|
+
|
391
|
+
# Run the main server loop.
|
392
|
+
#
|
393
|
+
# This is obviously blocking.
|
394
|
+
def run!
|
395
|
+
log :info, "UDP server loop at #{@address}:#{@port}"
|
396
|
+
@run = true
|
397
|
+
Socket.udp_server_loop(@address, @port) do |msg, src|
|
398
|
+
break unless @run
|
399
|
+
|
400
|
+
addr = src.remote_address
|
401
|
+
tag = "[#{addr.ip_address}:#{addr.ip_port.to_s.ljust(5)}]"
|
402
|
+
log :info, "#{tag} New initial packet received"
|
403
|
+
|
404
|
+
begin
|
405
|
+
pkt = Packet.parse(msg)
|
406
|
+
rescue ParseError => e
|
407
|
+
log :warn, "#{tag} Packet parse error: #{e.to_s}"
|
408
|
+
next
|
409
|
+
end
|
410
|
+
|
411
|
+
log :debug, "#{tag} -> PKT: #{pkt.inspect}"
|
412
|
+
tid = get_tid
|
413
|
+
tag = "[#{addr.ip_address}:#{addr.ip_port.to_s.ljust(5)}:#{tid.to_s.ljust(5)}]"
|
414
|
+
sock = addr.connect_from(@address, tid)
|
415
|
+
@clients[tid] = tag
|
416
|
+
|
417
|
+
unless pkt.is_a?(Packet::RRQ) || pkt.is_a?(Packet::WRQ)
|
418
|
+
log :warn, "#{tag} Bad initial packet: #{pkt.class}"
|
419
|
+
sock.send(Packet::ERROR.new(4, 'Illegal TFTP operation.').encode, 0)
|
420
|
+
sock.close
|
421
|
+
next
|
422
|
+
end
|
423
|
+
|
424
|
+
Thread.new do
|
425
|
+
@handler.run!(tag, pkt, sock, src)
|
426
|
+
@clients.delete(tid)
|
427
|
+
log :info, "#{tag} Session ended"
|
428
|
+
end
|
429
|
+
end
|
430
|
+
log :info, 'UDP server loop has stopped'
|
431
|
+
end
|
432
|
+
|
433
|
+
# Stop the main server loop.
|
434
|
+
#
|
435
|
+
# This will allow the currently pending sessions to finish.
|
436
|
+
def stop
|
437
|
+
log :info, 'Stopping UDP server loop'
|
438
|
+
@run = false
|
439
|
+
UDPSocket.new.send('break', 0, @address, @port)
|
440
|
+
end
|
441
|
+
|
442
|
+
private
|
443
|
+
# Get the server's TID.
|
444
|
+
#
|
445
|
+
# The TID is basically a random port number we will use for a session.
|
446
|
+
# This actually tries to get a unique TID per session.
|
447
|
+
# It uses only ports 1024 - 65535 as not to require root.
|
448
|
+
def get_tid
|
449
|
+
tid = 1024 + rand(64512)
|
450
|
+
tid = 1024 + rand(64512) while @clients.has_key? tid
|
451
|
+
tid
|
452
|
+
end
|
453
|
+
|
454
|
+
def log(level, msg)
|
455
|
+
@logger.send(level, msg) if @logger
|
456
|
+
end
|
457
|
+
end
|
458
|
+
|
459
|
+
# Basic read-write TFTP server.
|
460
|
+
#
|
461
|
+
# This is what most other TFTPd implementations give you.
|
462
|
+
class RWSimple < Base
|
463
|
+
def initialize(path, opts = {})
|
464
|
+
handler = Handler::RWSimple.new(path, opts)
|
465
|
+
super(handler, opts)
|
466
|
+
end
|
467
|
+
end
|
468
|
+
end
|
469
|
+
end
|
data/lib/netconfgen.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'netconfgen/netconfgen'
|
data/netconfgen.gemspec
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
#
|
3
|
+
|
4
|
+
Gem::Specification.new do |s|
|
5
|
+
s.name = 'netconfgen'
|
6
|
+
s.version = '0.0.1'
|
7
|
+
s.date = Time.now
|
8
|
+
|
9
|
+
s.summary = %q{Template based config generation}
|
10
|
+
s.files = `git ls-files`.split("\n")
|
11
|
+
s.executables = ['confgen']
|
12
|
+
s.test_files = s.files.grep(%r{^test/})
|
13
|
+
s.require_paths = ['lib']
|
14
|
+
s.authors = "Juho Mäkinen juho.makinen@gmail.com"
|
15
|
+
|
16
|
+
s.required_ruby_version = '>= 2.1.0'
|
17
|
+
|
18
|
+
s.add_development_dependency 'rubygems-tasks', '~> 0.2'
|
19
|
+
s.add_development_dependency 'minitest', '~> 5.4'
|
20
|
+
s.add_development_dependency 'rake', '~> 10.0'
|
21
|
+
end
|
data/netconfgen.yaml
ADDED
data/test/blockreader.rb
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'minitest/autorun'
|
2
|
+
|
3
|
+
require 'netconfgen'
|
4
|
+
|
5
|
+
class BlockEngine < Minitest::Test
|
6
|
+
|
7
|
+
def test_block_loading
|
8
|
+
|
9
|
+
br = NetConfGen::BlockEngine.new('test/data')
|
10
|
+
block = br.load('file1')
|
11
|
+
assert_equal block.code, "! this is a code module\n"
|
12
|
+
end
|
13
|
+
|
14
|
+
def test_block_variables
|
15
|
+
|
16
|
+
br = NetConfGen::BlockEngine.new('test/data')
|
17
|
+
|
18
|
+
br.set('foo', 1)
|
19
|
+
br.set('bar', 'hello')
|
20
|
+
block = br.load('variables')
|
21
|
+
assert_equal block.render, "foo: 1\nbar: hello\n"
|
22
|
+
end
|
23
|
+
|
24
|
+
|
25
|
+
def test_recursive_block_rendering
|
26
|
+
br = NetConfGen::BlockEngine.new('test/data')
|
27
|
+
block = br.load('file2')
|
28
|
+
|
29
|
+
assert_equal block.render,
|
30
|
+
%{! main
|
31
|
+
! this is a code module
|
32
|
+
|
33
|
+
! main again
|
34
|
+
}
|
35
|
+
|
36
|
+
end
|
37
|
+
end
|
data/test/data/file1.txt
ADDED
data/test/data/file2.txt
ADDED
data/test/data.json
ADDED
metadata
ADDED
@@ -0,0 +1,104 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: netconfgen
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Juho Mäkinen juho.makinen@gmail.com
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2019-07-06 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
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rake
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '10.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '10.0'
|
55
|
+
description:
|
56
|
+
email:
|
57
|
+
executables:
|
58
|
+
- confgen
|
59
|
+
extensions: []
|
60
|
+
extra_rdoc_files: []
|
61
|
+
files:
|
62
|
+
- ".gitignore"
|
63
|
+
- Gemfile
|
64
|
+
- Rakefile
|
65
|
+
- bin/confgen
|
66
|
+
- lib/netconfgen.rb
|
67
|
+
- lib/netconfgen/netconfgen.rb
|
68
|
+
- lib/netconfgen/version.rb
|
69
|
+
- netconfgen.gemspec
|
70
|
+
- netconfgen.yaml
|
71
|
+
- test/blockreader.rb
|
72
|
+
- test/data.json
|
73
|
+
- test/data/file1.txt
|
74
|
+
- test/data/file2.txt
|
75
|
+
- test/data/variables.txt
|
76
|
+
homepage:
|
77
|
+
licenses: []
|
78
|
+
metadata: {}
|
79
|
+
post_install_message:
|
80
|
+
rdoc_options: []
|
81
|
+
require_paths:
|
82
|
+
- lib
|
83
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
84
|
+
requirements:
|
85
|
+
- - ">="
|
86
|
+
- !ruby/object:Gem::Version
|
87
|
+
version: 2.1.0
|
88
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
89
|
+
requirements:
|
90
|
+
- - ">="
|
91
|
+
- !ruby/object:Gem::Version
|
92
|
+
version: '0'
|
93
|
+
requirements: []
|
94
|
+
rubyforge_project:
|
95
|
+
rubygems_version: 2.5.2
|
96
|
+
signing_key:
|
97
|
+
specification_version: 4
|
98
|
+
summary: Template based config generation
|
99
|
+
test_files:
|
100
|
+
- test/blockreader.rb
|
101
|
+
- test/data.json
|
102
|
+
- test/data/file1.txt
|
103
|
+
- test/data/file2.txt
|
104
|
+
- test/data/variables.txt
|