Commit ed57027e authored by Colin Shea's avatar Colin Shea

multiple changes all rolled into 1

- delete environment and hash_ext
- add CAA record support
- generate code coverage reports with SimpleCov
- strip trailing whitespace
- fix SSHFP regex
- add CAA tests, add a test that zonefile can parse it's own output
parent 6639d520
......@@ -3,3 +3,4 @@
/data
/tmp
/example/generated/
/coverage
#!/usr/bin/env ruby
require_relative '../lib/environment'
require 'yaml'
$: << File.expand_path(File.dirname(__FILE__) + '/../lib')
require "zone"
require "zone_generator"
require 'zonefile'
generator = ZoneGenerator.new Dir.pwd
generator.generate
......
require 'rubygems'
require 'bundler/setup'
require 'yaml'
require_relative "hash_ext"
require_relative "zone"
require_relative "zone_generator"
require_relative 'zonefile'
class Hash
def symbolize_keys!
keys.each do |key|
self[(key.to_sym rescue key) || key] = delete(key)
end
self
end
def deep_symbolize_keys!
symbolize_keys!
# symbolize each hash in .values
values.each{|h| h.deep_symbolize_keys! if h.is_a?(Hash) }
# symbolize each hash inside an array in .values
values.select{|v| v.is_a?(Array) }.flatten.each{|h| h.deep_symbolize_keys! if h.is_a?(Hash) }
self
end
end
......@@ -197,6 +197,22 @@ class Zone
push :txt, "@", ttl, text: text
end
# caa 'issue', 'letsencrypt.org'
# caa 'issue', ['letsencrypt.org', 'digicert.com']
# caa 'issuewild', nil
# caa 'iodef', 'mailto:foo@example.com'
# caa 'iodef', 'https://example.com/iodef'
def caa(property, valuelist, ttl=nil)
values = Array(valuelist)
if values.empty?
push :caa, "@", ttl, flags: 0, property: property, value: ';'
else
values.each do |ca_auth|
push :caa, "@", ttl, flags: 0, property: property, value: ca_auth
end
end
end
def record?(type, name)
return @zonefile.records[type].any? do |record|
record[:name] == name
......
......@@ -4,7 +4,6 @@ class ZoneGenerator
def initialize(basedir)
@config = YAML.load_file("config.yaml")
@config.deep_symbolize_keys!
@soa = {
origin: "@",
ttl: "86400",
......@@ -14,11 +13,11 @@ class ZoneGenerator
retry: "2H",
expire: "1W",
minimumTTL: "11h"
}.merge(@config[:soa])
}.merge(@config['soa'])
@zones_dir = File.expand_path(@config[:ruby_zones])
@template_dir = File.expand_path(@config[:templates])
@generated = File.expand_path(@config[:output])
@zones_dir = File.expand_path(@config['ruby_zones'])
@template_dir = File.expand_path(@config['templates'])
@generated = File.expand_path(@config['output'])
@tmp_named = "#{@generated}/named.conf"
@tmp_zones = "#{@generated}/zones"
......@@ -41,7 +40,7 @@ class ZoneGenerator
puts "Generating zone for '#{domain}'"
generate_zone(file, domain)
f.puts "zone \"#{domain}\" IN { type master; file \"#{@config[:zones_dir]}/#{domain}\"; };"
f.puts "zone \"#{domain}\" IN { type master; file \"#{@config['zones_dir']}/#{domain}\"; };"
end
end
end
......@@ -53,7 +52,7 @@ class ZoneGenerator
new_zonefile = zone.zonefile
# path to the deployed version
old_file = "#{@config[:zones_dir]}/#{domain}.zone"
old_file = "#{@config['zones_dir']}/#{domain}.zone"
# is there already a deployed version?
if File.exists?(old_file)
......@@ -86,7 +85,7 @@ class ZoneGenerator
end
def deploy
cmd = @config[:execute]
cmd = @config['execute']
print "Executing '#{cmd}' ... "
out = `#{cmd}`
puts "done"
......
......@@ -2,9 +2,9 @@
# = Ruby Zonefile - Parse and manipulate DNS Zone Files.
#
# == Description
# This class can read, manipulate and create DNS zone files. It supports A, AAAA, MX, NS, SOA,
# This class can read, manipulate and create DNS zone files. It supports A, AAAA, MX, NS, SOA,
# TXT, CNAME, PTR and SRV records. The data can be accessed by the instance method of the same
# name. All except SOA return an array of hashes containing the named data. SOA directly returns the
# name. All except SOA return an array of hashes containing the named data. SOA directly returns the
# hash since there can only be one SOA information.
#
# The following hash keys are returned per record type:
......@@ -48,13 +48,15 @@
# - :name, :ttl, :class, :text
# * SSHFP
# - :name, :ttl, :class, :key_type, :fingerprint_type, :fingerprint
# * CAA
# - :name, :ttl, :class, :flag, :property, :value
#
# == Examples
#
# === Read a Zonefile
#
# zf = Zonefile.from_file('/path/to/zonefile.db')
#
#
# # Display MX-Records
# zf.mx.each do |mx_record|
# puts "Mail Exchagne with priority: #{mx_record[:pri]} --> #{mx_record[:host]}"
......@@ -96,46 +98,46 @@
# You can switch this off globally by calling Zonefile.preserve_name(false)
#
# == Authors
#
# Martin Boese, based on Simon Flack Perl library DNS::ZoneParse
#
# Martin Boese, based on Simon Flack Perl library DNS::ZoneParse
#
# Andy Newton, patch to support various additional records
#
class Zonefile
RECORDS = %w{ mx a aaaa ns cname txt ptr srv soa ds dnskey rrsig nsec nsec3 nsec3param tlsa naptr spf sshfp }
RECORDS = %w{ mx a aaaa ns cname txt ptr srv soa ds dnskey rrsig nsec nsec3 nsec3param tlsa naptr spf sshfp caa }
attr :records
attr :soa
attr :data
# global $ORIGIN option
attr :origin
attr :origin
# global $TTL option
attr :ttl
@@preserve_name = true
# For compatibility: This can switches off copying of the :name from the
# previous record in a zonefile if found omitted.
# This was zonefile's behavior in <= 1.03 .
# For compatibility: This can switches off copying of the :name from the
# previous record in a zonefile if found omitted.
# This was zonefile's behavior in <= 1.03 .
def self.preserve_name(do_preserve_name)
@@preserve_name = do_preserve_name
end
def method_missing(m, *args)
mname = m.to_s.sub("=","")
return super unless RECORDS.include?(mname)
if m.to_s[-1].chr == '=' then
@records[mname.intern] = args.first
@records[mname.intern]
else
else
@records[m]
end
end
# Compact a zonefile content - removes empty lines, comments,
# Compact a zonefile content - removes empty lines, comments,
# converts tabs into spaces etc...
def self.simplify(zf)
# concatenate everything split over multiple lines in parentheses - remove ;-comments in block
......@@ -162,13 +164,13 @@ class Zonefile
@data = zonefile
@filename = file_name
@origin = origin || (file_name ? file_name.split('/').last : '')
@records = {}
@soa = {}
RECORDS.each { |r| @records[r.intern] = [] }
parse
end
# True if no records (except sao) is defined in this file
def empty?
RECORDS.each do |r|
......@@ -176,12 +178,12 @@ class Zonefile
end
true
end
# Create a new object by reading the content of a file
def self.from_file(file_name, origin = nil)
Zonefile.new(File.read(file_name), file_name.split('/').last, origin)
end
def add_record(type, data= {})
if @@preserve_name then
@lastname = data[:name] if data[:name].to_s != ''
......@@ -189,7 +191,7 @@ class Zonefile
end
@records[type.downcase.intern] << data
end
# Generates a new serial number in the format of YYYYMMDDII if possible
def new_serial
base = "%04d%02d%02d" % [Time.now.year, Time.now.month, Time.now.day ]
......@@ -199,12 +201,12 @@ class Zonefile
@soa[:serial] = ns.to_s
return ns.to_s
end
ii = 0
while (("#{base}%02d" % ii).to_i <= @soa[:serial].to_i) do
ii += 1
end
@soa[:serial] = "#{base}%02d" % ii
@soa[:serial] = "#{base}%02d" % ii
end
def parse_line(line)
......@@ -231,7 +233,7 @@ class Zonefile
elsif line=~/^(#{valid_name})? \s*
#{ttl_cls}
AAAA \s
(#{valid_ip6})
(#{valid_ip6})
/x then
add_record('aaaa', :name => $1, :ttl => $2, :class => $3, :host => $4)
elsif line=~/^(#{valid_name})? \s*
......@@ -372,20 +374,34 @@ class Zonefile
add_record('txt', :name => $1, :ttl => $2, :class => $3, :text => $4.strip)
elsif line =~ /^(#{valid_name})? \s* #{ttl_cls} SPF \s+ (.*)$/ix
add_record('spf', :name => $1, :ttl => $2, :class => $3, :text => $4.strip)
elsif line =~ /^(#{valid_name})? \s* #{ttl_cls} SSHFP (\d) (\d) (.*)$/ix
add_record('sshfp', :name => $1, :ttl => $2, :class => $3, key_type: $4, fingerprint_type: $5, fingerprint: $6.strip)
elsif line =~ /\$TTL\s+(#{rr_ttl})/i
elsif line=~/^(#{valid_name})? \s*
#{ttl_cls}
SSHFP \s+
(\d+) \s+
(\d+) \s+
#{hexadeimal}
/ix
add_record('sshfp', :name => $1, :ttl => $2, :class => $3, key_type: $4.to_i, fingerprint_type: $5.to_i, fingerprint: $6.strip)
elsif line=~/^(#{valid_name})? \s*
#{ttl_cls}
CAA \s+
(\d+) \s+
(\w+) \s+
#{quoted}
/ix
add_record('caa', :name => $1, :ttl => $2, :class => $3, flags: $4.to_i, property: $5, value: $6.strip)
elsif line =~ /\$TTL\s+(#{rr_ttl})/i
@ttl = $1
end
end
def parse
Zonefile.simplify(@data).each_line do |line|
parse_line(line)
parse_line(line)
end
end
# Build a new nicely formatted Zonefile
#
def output
......@@ -416,16 +432,16 @@ ENDH
self.mx.each do |mx|
out << "#{mx[:name]} #{mx[:ttl]} #{mx[:class]} MX #{mx[:pri]} #{mx[:host]}\n"
end
out << "\n; Zone A Records\n" unless self.a.empty?
self.a.each do |a|
out << "#{a[:name]} #{a[:ttl]} #{a[:class]} A #{a[:host]}\n"
end
end
out << "\n; Zone CNAME Records\n" unless self.cname.empty?
self.cname.each do |cn|
out << "#{cn[:name]} #{cn[:ttl]} #{cn[:class]} CNAME #{cn[:host]}\n"
end
end
out << "\n; Zone AAAA Records\n" unless self.aaaa.empty?
self.aaaa.each do |aaaa|
......@@ -446,7 +462,7 @@ ENDH
self.srv.each do |srv|
out << "#{srv[:name]} #{srv[:ttl]} #{srv[:class]} SRV #{srv[:pri]} #{srv[:weight]} #{srv[:port]} #{srv[:host]}\n"
end
out << "\n; Zone PTR Records\n" unless self.ptr.empty?
self.ptr.each do |ptr|
out << "#{ptr[:name]} #{ptr[:ttl]} #{ptr[:class]} PTR #{ptr[:host]}\n"
......
......@@ -63,3 +63,14 @@ _443._tcp.www.example.com. 86400 IN TLSA (
1b177615d466f6c4b71c216a50292bd5
8c9ebdd2f74e38fe51ffd48c43326cbc )
urn.example.com. IN NAPTR 100 50 "s" "http+N2L+N2C+N2R" "" www.example.com.
@ IN CAA 128 issue "letsencrypt.org"
@ IN CAA 0 issue "comodoca.com"
@ IN CAA 1 issuewild ";"
@ IN CAA 0 iodef "mailto:caa@example.com"
www.example.com IN SSHFP 1 1 5f2f2e0676798a0273572bc77b99d6319a560fd5
www.example.com IN SSHFP 1 2 f5ae7764148c8f587996e5be3324286bdd1e9b935caaf3ff0ed3c9bbc0152097
www.example.com IN SSHFP 2 1 9b913ce5339f8761c26a2ed755156d4785042b2d
www.example.com IN SSHFP 2 2 15477282e6a510a6c534e61f1df40d3750edcf86c6f4bf2ab5a964ccada7be3d
www.example.com IN SSHFP 3 1 1262006f9a45bb36b1aa14f45f354b694b77d7c3
www.example.com IN SSHFP 3 2 e5921564252fe10d2dbafeb243733ed8b1d165b8fa6d5a0e29198e5793f0623b
......@@ -4,9 +4,13 @@ require 'yaml'
require 'minitest/autorun'
require 'minitest/pride'
require 'simplecov'
SimpleCov.start do
add_filter "tests/"
end
$: << File.expand_path(File.dirname(__FILE__) + '/../lib')
require "hash_ext"
require "zone"
require "zone_generator"
require 'zonefile'
......
......@@ -191,6 +191,24 @@ describe Zone do
end
end
describe "caa record" do
it "should create a single record" do
subject.caa 'issue', 'letsencrypt.org'
subject.zonefile.caa.must_equal [{:class=>"IN", :name=>"@", :ttl=>nil, :flags=>0, :property=>'issue', :value=>"letsencrypt.org"}]
end
it "should create multiple records" do
subject.caa 'issue', ['letsencrypt.org', 'digicert.com']
subject.zonefile.caa.must_equal [{:class=>"IN", :name=>"@", :ttl=>nil, :flags=>0, :property=>'issue', :value=>"letsencrypt.org"},
{:class=>"IN", :name=>"@", :ttl=>nil, :flags=>0, :property=>'issue', :value=>"digicert.com"}]
end
it "should default to semicolon" do
subject.caa 'issuewild', nil
subject.zonefile.caa.must_equal [{:class=>"IN", :name=>"@", :ttl=>nil, :flags=>0, :property=>'issuewild', :value=>";"}]
end
end
describe "record? helper" do
it "should return true for existing records" do
subject.cname 'www', "other-server."
......
require 'zonefile'
$zonefile = ARGV[0] || 'example.zone'
class TC_Zonefile < Minitest::Test
def setup
@zf = Zonefile.from_file(File.dirname(__FILE__) + '/'+$zonefile, 'test-origin')
@zf = Zonefile.from_file(File.dirname(__FILE__) + '/example.zone', 'test-origin')
end
def swap # generate output and re-read @zf from it
......@@ -17,12 +15,12 @@ class TC_Zonefile < Minitest::Test
zf.soa[:refresh] = 1234
assert zf.empty?
end
def test_setter
data = [ { :class => 'IN', :name => '123', :host => 'test' },
{ :name => '321', :hosts => 'test2' } ]
@zf.ptr = data
assert_equal 2, @zf.ptr.size
assert_equal 2, @zf.ptr.size
assert @zf.ptr[0][:host] == data[0][:host]
assert @zf.ptr[1][:name] == data[1][:name]
assert_raises(NoMethodError) do
......@@ -39,7 +37,7 @@ class TC_Zonefile < Minitest::Test
assert_equal '2000100501', @zf.soa[:serial]
assert_equal 'support.dns-zoneparse-test.net.', @zf.soa[:email]
assert_equal 'ns0.dns-zoneparse-test.net.', @zf.soa[:primary]
begin
@swap_soa = true
swap
......@@ -63,7 +61,7 @@ class TC_Zonefile < Minitest::Test
test_a
end unless @swap_a
end
def test_preserve_name
Zonefile.preserve_name(false)
setup
......@@ -86,8 +84,8 @@ class TC_Zonefile < Minitest::Test
test_mx
end unless @swap_mx
end
def test_cname
def test_cname
assert !!@zf.cname.find { |e| e[:host] == 'www' }
begin
@swap_cname = true
......@@ -95,7 +93,7 @@ class TC_Zonefile < Minitest::Test
test_cname
end unless @swap_cname
end
def test_ns
assert_equal 'ns0.dns-zoneparse-test.net.', @zf.ns[0][:host]
assert_equal 'ns1.dns-zoneparse-test.net.', @zf.ns[1][:host]
......@@ -105,7 +103,7 @@ class TC_Zonefile < Minitest::Test
test_ns
end unless @swap_ns
end
def test_txt
#puts @zf.txt.inspect
assert_equal '"web;server"', @zf.txt[0][:text]
......@@ -116,7 +114,7 @@ class TC_Zonefile < Minitest::Test
assert_equal "\"t=y; o=-\"", @zf.txt[2][:text]
assert_equal 'maxnet.ao', @zf.txt[3][:text]
assert_equal '_kerberos', @zf.txt[3][:name]
assert_equal 4, @zf.txt.size
begin
@swap_txt = true
......@@ -124,7 +122,7 @@ class TC_Zonefile < Minitest::Test
test_txt
end unless @swap_txt
end
def test_spf
assert_equal '"v=spf1 mx ~all"', @zf.spf[0][:text]
assert_equal "IN", @zf.spf[0][:class]
......@@ -156,7 +154,7 @@ class TC_Zonefile < Minitest::Test
test_aaaa
end unless @swap_aaaa
end
def test_srv
assert_equal '_sip._tcp.example.com.', @zf.srv[0][:name]
assert_equal '86400', @zf.srv[0][:ttl]
......@@ -170,14 +168,14 @@ class TC_Zonefile < Minitest::Test
test_srv
end unless @swap_srv
end
def test_serial_generator
old = @zf.soa[:serial]
new = @zf.new_serial
assert new.to_i > old.to_i
newer = @zf.new_serial
assert newer.to_i - 1, new
@zf.soa[:serial] = '9999889901'
@zf.new_serial
assert_equal '9999889902', @zf.soa[:serial]
......@@ -185,7 +183,7 @@ class TC_Zonefile < Minitest::Test
def test_ptr
assert_equal '12.23.21.23.in-addr.arpa', @zf.ptr[0][:name]
assert_equal 'www.myhost.example.com.', @zf.ptr[0][:host]
assert_equal 'www.myhost.example.com.', @zf.ptr[0][:host]
begin
@swap_ptr = true
swap
......@@ -329,18 +327,45 @@ SIGNATURE
8c9ebdd2f74e38fe51ffd48c43326cbc
SIGNATURE
assert_equal sig, @zf.tlsa[0][:data].gsub( /\s+/,'')
begin
begin
@swap_tlsa= true
swap
test_tlsa
end unless @swap_tlsa
end
def test_caa
assert_equal "@", @zf.caa[0][:name]
assert_equal 128, @zf.caa[0][:flags]
assert_equal "issue", @zf.caa[0][:property]
assert_equal "\"letsencrypt.org\"", @zf.caa[0][:value]
assert_equal "@", @zf.caa[1][:name]
assert_equal 0, @zf.caa[1][:flags]
assert_equal "issue", @zf.caa[1][:property]
assert_equal "\"comodoca.com\"", @zf.caa[1][:value]
assert_equal "@", @zf.caa[2][:name]
assert_equal 1, @zf.caa[2][:flags]
assert_equal "issuewild", @zf.caa[2][:property]
assert_equal "\";\"", @zf.caa[2][:value]
assert_equal "@", @zf.caa[3][:name]
assert_equal 0, @zf.caa[3][:flags]
assert_equal "iodef", @zf.caa[3][:property]
assert_equal "\"mailto:caa@example.com\"", @zf.caa[3][:value]
end
def test_origin
assert_equal 'example.zone.', @zf.origin
swap
assert_equal 'example.zone.', @zf.origin
assert_equal 'example.zone.', @zf.origin
end
def test_self_parse
zf2 = Zonefile.new(@zf.output, 'example.zone')
assert_equal @zf.output, zf2.output
end
end
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment