master
Denis Knauf 2020-06-20 22:30:05 +02:00
commit 4fd46b4f0e
10 changed files with 603 additions and 0 deletions

8
.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
/.bundle/
/.yardoc
/_yardoc/
/coverage/
/doc/
/pkg/
/spec/reports/
/tmp/

6
Gemfile Normal file
View File

@ -0,0 +1,6 @@
source "https://rubygems.org"
# Specify your gem's dependencies in knot.gemspec
gemspec
gem "rake", "~> 12.0"

36
README.md Normal file
View File

@ -0,0 +1,36 @@
# Knot
Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/knot`. To experiment with that code, run `bin/console` for an interactive prompt.
TODO: Delete this and the text above, and describe your gem
## Installation
Add this line to your application's Gemfile:
```ruby
gem 'knot'
```
And then execute:
$ bundle install
Or install it yourself as:
$ gem install knot
## Usage
TODO: Write usage instructions here
## Development
After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
## Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/knot.

2
Rakefile Normal file
View File

@ -0,0 +1,2 @@
require "bundler/gem_tasks"
task :default => :spec

31
knot-ruby.gemspec Normal file
View File

@ -0,0 +1,31 @@
require_relative 'lib/knot/version'
Gem::Specification.new do |spec|
spec.name = "knot"
spec.version = Knot::VERSION
spec.authors = ['Denis Knauf']
spec.email = ['gems+knot@denkn.at']
spec.licenses = ["LGPL-3.0"]
spec.summary = %q{Provides interface to knot-server.}
spec.description = %q{Implements knot-protocol to provide an interface to knot-DNS-server}
spec.homepage = 'https://git.denkn.at/deac/knot'
spec.required_ruby_version = Gem::Requirement.new(">= 2.3.0")
#spec.metadata["allowed_push_host"] = "TODO: Set to 'http://mygemserver.com'"
spec.metadata["homepage_uri"] = spec.homepage
spec.metadata["source_code_uri"] = spec.homepage
spec.metadata["changelog_uri"] = spec.homepage
spec.add_dependency 'iounpack', '~> 0'
# Specify which files should be added to the gem when it is released.
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
end
spec.bindir = "bin"
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
spec.require_paths = ["lib"]
end

4
lib/knot.rb Normal file
View File

@ -0,0 +1,4 @@
require 'knot/version'
require 'knot/errors'
require 'knot/protocol'
require 'knot/interface'

166
lib/knot/errors.rb Normal file
View File

@ -0,0 +1,166 @@
module Knot
class Error < ::Exception
attr_reader :Errno
attr_reader :Key
attr_reader :Errstr
end
end
module Knot::Errors
@num2exc = {}
@key2exc = {}
@err2exc = {}
class << self
attr_reader :num2exc, :key2exc, :err2exc
def array_arguments_with_typecheck *args
l = args.length
lambda do |a|
l == a.length and
args.zip(a).all? {|t,v| t === v }
end
end
end
def method_missing key
self[key]
end
def [] key_or_errno
case key_or_errno
when Integer
@num2exc[key_or_errno]
when Exception
key_or_errno
when Symbol
@key2exc[key_or_errno]
else
raise ArgumentError, "Invalid type. Expect Integer/Knot::Error/Symbol"
end
end
i = 0
[
[:EOK, 0, "OK"],
# Directly mapped error codes.
[:ENOMEM, -Errno::ENOMEM::Errno, "not enough memory" ],
[:EINVAL, -Errno::EINVAL::Errno, "invalid parameter" ],
[:ENOTSUP, -Errno::ENOTSUP::Errno, "operation not supported" ],
[:EBUSY, -Errno::EBUSY::Errno, "requested resource is busy" ],
[:EAGAIN, -Errno::EAGAIN::Errno, "OS lacked necessary resources" ],
[:EACCES, -Errno::EACCES::Errno, "operation not permitted" ],
[:ECONNREFUSED, -Errno::ECONNREFUSED::Errno, "connection refused" ],
[:EISCONN, -Errno::EISCONN::Errno, "already connected" ],
[:EADDRINUSE, -Errno::EADDRINUSE::Errno, "address already in use" ],
[:ENOENT, -Errno::ENOENT::Errno, "not exists" ],
[:EEXIST, -Errno::EEXIST::Errno, "already exists" ],
[:ERANGE, -Errno::ERANGE::Errno, "value is out of range" ],
[:EADDRNOTAVAIL, -Errno::EADDRNOTAVAIL::Errno, "address is not available" ],
# General errors.
[:ERROR, -1000, "failed"],
[:EPARSEFAIL, "parser failed"],
[:ESEMCHECK, "semantic check"],
[:EUPTODATE, "zone is up-to-date"],
[:EFEWDATA, "not enough data to parse"],
[:ESPACE, "not enough space provided"],
[:EMALF, "malformed data"],
[:ENSEC3PAR, "missing or wrong NSEC3PARAM record"],
[:ENSEC3CHAIN, "missing or wrong NSEC3 chain in the zone"],
[:EOUTOFZONE, "name does not belong to the zone"],
[:EZONEINVAL, "invalid zone file"],
[:ENOZONE, "no such zone found"],
[:ENONODE, "no such node in zone found"],
[:ENORECORD, "no such record in zone found"],
[:EISRECORD, "such record already exists in zone"],
[:ENOMASTER, "no usable master"],
[:EPREREQ, "UPDATE prerequisity not met"],
[:ETTL, "TTL mismatch"],
[:ENOXFR, "transfer was not sent"],
[:EDENIED, "not allowed"],
[:ECONN, "connection reset"],
[:ETIMEOUT, "connection timeout"],
[:ENODIFF, "cannot create zone diff"],
[:ENOTSIG, "expected a TSIG or SIG(0)"],
[:ELIMIT, "exceeded response rate limit"],
[:EZONESIZE, "zone size exceeded"],
[:EOF, "end of file"],
[:ESYSTEM, "system error"],
[:EFILE, "file error"],
[:ESOAINVAL, "SOA mismatch"],
[:ETRAIL, "trailing data"],
[:EPROCESSING, "processing error"],
# Control states.
[:CTL_ESTOP, "stopping server"],
# Network errors.
[:NET_EADDR, "bad address or host name"],
[:NET_ESOCKET, "can't create socket"],
[:NET_ECONNECT, "can't connect"],
[:NET_ESEND, "can't send data"],
[:NET_ERECV, "can't receive data"],
[:NET_ETIMEOUT, "network timeout"],
# Encoding errors.
[:BASE64_ESIZE, "invalid base64 string length"],
[:BASE64_ECHAR, "invalid base64 character"],
[:BASE32HEX_ESIZE, "invalid base32hex string length"],
[:BASE32HEX_ECHAR, "invalid base32hex character"],
# TSIG errors.
[:KNOT_TSIG_EBADSIG, "failed to verify TSIG"],
[:KNOT_TSIG_EBADKEY, "TSIG key not recognized or invalid"],
[:KNOT_TSIG_EBADTIME, "TSIG out of time window"],
[:KNOT_TSIG_EBADTRUNC, "TSIG bad truncation"],
# DNSSEC errors.
[:DNSSEC_ENOKEY, "no keys for signing"],
[:DNSSEC_EMISSINGKEYTYPE, "missing active KSK or ZSK"],
# Yparser errors.
[:YP_ECHAR_TAB, "tabulator character is not allowed"],
[:YP_EINVAL_ITEM, "invalid item"],
[:YP_EINVAL_ID, "invalid identifier"],
[:YP_EINVAL_DATA, "invalid value"],
[:YP_EINVAL_INDENT, "invalid indentation"],
[:YP_ENOTSUP_DATA, "value not supported"],
[:YP_ENOTSUP_ID, "identifier not supported"],
[:YP_ENODATA, "missing value"],
[:YP_ENOID, "missing identifier"],
# Configuration errors.
[:CONF_ENOTINIT, "config DB not initialized"],
[:CONF_EVERSION, "invalid config DB version"],
[:CONF_EREDEFINE, "duplicate identifier"],
# Transaction errors.
[:TXN_EEXISTS, "too many transactions"],
[:TXN_ENOTEXISTS, "no active transaction"],
# DNSSEC errors.
[:INVALID_PUBLIC_KEY, "invalid public key"],
[:INVALID_PRIVATE_KEY, "invalid private key"],
[:INVALID_KEY_ALGORITHM, "invalid key algorithm"],
[:INVALID_KEY_SIZE, "invalid key size"],
[:INVALID_KEY_ID, "invalid key ID"],
[:INVALID_KEY_NAME, "invalid key name"],
[:NO_PUBLIC_KEY, "no public key"],
[:NO_PRIVATE_KEY, "no private key"],
].each do |v|
e = nil
case v
when array_arguments_with_typecheck( Symbol, String)
v, e = v
when array_arguments_with_typecheck( String, String)
v, e = v
v = v.to_sym
when array_arguments_with_typecheck( Symbol, Integer, String)
v, i, e = v
when array_arguments_with_typecheck( String, Integer, String)
v, i, e = v
v = v.to_sym
else
raise ArgumentError, "[Symbol, String] | [Symbol, Int, String] expected, not #{v}"
end
cl = Class.new Exception
cl.const_set :Key, v
cl.const_set :Errno, i
cl.const_set :Errstr, e
const_set v, cl
@num2exc[i] = @key2exc[v] = @err2exc[e] = cl
i += 1
end
end

159
lib/knot/interface.rb Normal file
View File

@ -0,0 +1,159 @@
require_relative 'protocol'
require_relative 'errors'
module Knot
end
class Knot::Zone
attr_reader :protocol, :zone
def initialize zone, protocol = nil
@protocol = protocol || Protocol.new
@zone, @transaction_opened = zone, 0
end
def begin
@transaction_opened += 1
@protocol.call command: 'zone-begin', zone: @zone if 1 == @transaction_opened
end
def commit
@protocol.call command: 'zone-commit', zone: @zone if 1 == @transaction_opened
@transaction_opened -= 1 if 0 < @transaction_opened
end
def abort
@protocol.call command: 'zone-abort', zone: @zone
@transaction_opened = 0
end
def transaction
self.begin
yield self
rescue Object
self.abort
raise
ensure
self.commit unless $!
end
# zone operation
def check() @protocol.call command: 'zone-check', zone: @zone end
def reload() @protocol.call command: 'zone-reload', zone: @zone end
def refresh() @protocol.call command: 'zone-refresh', zone: @zone end
def notify() @protocol.call command: 'zone-notify', zone: @zone end
def retransfer() @protocol.call command: 'zone-retransfer', zone: @zone end
def sign() @protocol.call command: 'zone-sign', zone: @zone end
def freeze() @protocol.call command: 'zone-freeze', zone: @zone end
def thaw() @protocol.call command: 'zone-thaw', zone: @zone end
def status( filter = nil) @protocol.call command: 'zone-status', zone: @zone, filter: filter end
def stats( modul = nil, counter = nil)
@protocol.call command: 'zone-stats', zone: @zone, module: modul, counter: counter
end
# zone manipulation
def read() @protocol.call command: 'zone-read', zone: @zone end
def diff() @protocol.call command: 'zone-diff', zone: @zone end
# setting record
# if data is nil, it will be unset.
def set owner, ttl = nil, type, data
@protocol.call command: data.nil? ? 'zone-unset' : 'zone-set',
zone: @zone, owner: owner, ttl: ttl, type: type, data: data
rescue Knot::Errors::EISRECORD, Knot::Errors::ENONODE, Knot::Errors::ENOENT
end
alias []= set
def unset owner, type = nil, data = nil
@protocol.call command: 'zone-unset',
zone: @zone, owner: owner, type: type, data: data
rescue Knot::Errors::ENONODE, Knot::Errors::ENOENT
end
alias delete unset
def get owner = nil, type = nil
@protocol.call command: 'zone-get',
zone: @zone, owner: owner, type: type
rescue Knot::Errors::ENONODE, Knot::Errors::ENOENT
nil
end
alias [] get
end
class Knot::Conf
def initialize protocol = nil
@protocol = protocol || Protocol.new
@transaction_opened = 0
end
def begin
@transaction_opened += 1
@protocol.call command: 'conf-begin' if 1 == @transaction_opened
end
def commit
@protocol.call command: 'conf-commit' if 1 == @transaction_opened
@transaction_opened -= 1 if 0 < @transaction_opened
end
def abort
@protocol.call command: 'conf-abort'
@transaction_opened = 0
end
def transaction
self.begin
yield self
rescue Object
self.abort
raise
ensure
self.commit
end
def parse_item k
case k
when k
case k.keys.sort
when %w[section], %w[id section], %w[item section], %w[id item section] then k
else raise ArgumentError, "Invalid Item-format"
end
when Array
case k.length
when 1 then {section: k[0]}
when 2 then {section: k[0], item: k[1]}
when 3 then {section: k[0], id: k[1], item: k[2]}
else raise ArgumentError, "Invalid Item-format"
end
when /\A
(?<section> [a-z0-9_-]+ )
(?: \[ (?<id> [a-z0-9_.-]+) \] )?
(?: \. (?<item>[a-z0-9_-]+) )?
\z/xi
$~.named_captures.delete_if {|_,v| v.nil? }
else raise ArgumentError, "Invalid Item-format"
end
end
def set item, value
@protocol.call parse_item( item).update( command: 'conf-set', data: value)
end
alias [] set
def unset item, value = nil
@protocol.call parse_item( item).update( command: 'conf-unset', data: value)
end
alias delete unset
def list item = nil
@protocol.call (item ? parse_item( item) : {}).update( command: 'conf-list')
end
def read item = nil
@protocol.call (item ? parse_item( item) : {}).update( command: 'conf-read')
end
end

188
lib/knot/protocol.rb Normal file
View File

@ -0,0 +1,188 @@
require 'iounpack'
require_relative 'errors'
module Knot
class Protocol
end
end
module Knot::Protocol::Type
def [] code
case code
when 0, End then return End
when 1, Data then return Data
when 2, Extra then return Extra
when 3, Block then return Block
end
end
class Base
def self.expect_data?
false
end
def expect_data?() self.class.expect_data? end
def code() self.class.code end
end
class End < Base
def self.code() 0 end
end
class Data < Base
def code() 1 end
def expect_data?
true
end
attr_reader :data
def initialize data
@data = data
end
end
class Extra < Base
def code() 2 end
def expect_data?
true
end
attr_reader :data
def initialize data
@data = data
end
end
class Block < Base
def code() 3 end
end
end
module Knot::Protocol::Idx
Idx = [
:command, # 10, :CMD, # Control command name.
:flags, # 11, :FLAGS, # Control command flags.
:error, # 12, :ERROR, # Error message.
:section, # 13, :SECTION, # Configuration section name.
:item, # 14, :ITEM, # Configuration item name.
:id, # 15, :ID, # Congiguration item identifier.
:zone, # 16, :ZONE, # Zone name.
:owner, # 17, :OWNER, # Zone record owner
:ttl, # 18, :TTL, # Zone record TTL.
:type, # 19, :TYPE, # Zone record type name.
:data, # 1a, :DATA, # Configuration item/zone record data.
:filter, # 1b, :FILTER, # An option or a filter for output data processing.
]
Name = {}
Code = {}
Idx.each_with_index do |v, i|
Code[0x10+i] = v
Name[v] = i
end
def self.[] k
case k
when Symbol
Name[k] or raise Knot::Errors::EINVAL, "Unknown Idx: #{k}"
when Integer
Idx[k] or raise Knot::Errors::EINVAL, "Unknown Idx: #{k}"
else
raise ArgumentError, "Unknown Idx-Type: #{k}"
end
end
end
class Knot::Protocol
attr_reader :sock, :conf, :zones
attr_accessor :debug
def initialize path_or_sock = nil
case path_or_sock
when String, Pathname
@sock = UNIXSocket.new path_or_sock.to_s
when Socket
@sock = path_or_sock
when nil
@sock = UNIXSocket.new '/run/knot/knot.sock'
end
@debug = false
@conf = Knot::Conf.new self
@zones = Hash.new {|h, zone| h[zone] = Knot::Zone.new zone, self }
end
def snd sock: nil, **data
rsock = sock || @sock
s = ''
sock = StringIO.new s
sock.write [1].pack( 'c')
data[:flags] ||= ''
Idx::Idx.each_with_index do |n, i|
v = data[n]&.to_s
sock.write [0x10+i, v.size, v].pack( 'c na*') if v
end
sock.write [3].pack( 'c')
sock.flush
STDERR.puts( {data: data, _: s}.inspect) if @debug
rsock.write s
rsock.flush
end
class RecordIO
attr_reader :str
def initialize sock, str = nil
@str, @sock = str || '', sock
end
def unpack pattern
IOUnpack.new(pattern).unpack self
end
def unpack1 pattern
IOUnpack.new(pattern).unpack1 self
end
def read n
s = @sock.read n
@str.insert -1, s
s
end
end
def rcv sock: nil
ret, r = [], nil
sock = sock || @sock
sock = RecordIO.new sock if @debug
loop do
t = sock.unpack1 'c'
case t
when 0, 3
return ret
when 1, 2
type = t
ret.push( r = {})
else
raise Knot::Errors::EINVAL, "Missing Type before: #{t}" if ret.empty?
i = Idx::Idx[t - 0x10] or raise Knot::Errors::EINVAL, "Unknown index: #{t-0x10}"
l = sock.unpack1 'n'
r[i] = sock.read( l)
end
end
ensure
STDERR.puts( {rcvd: ret, read: sock.str}.inspect) if @debug
ret
end
def call sock: nil, **data
snd sock: sock, **data
rcv( sock: sock).each do |r|
if r[:error]
if e = Knot::Errors.err2exc[r[:error]]
raise e, r[:error]
end
raise Knot::Error, r[:error]
end
end
end
def zone( zone) @zones[zone.to_s.to_sym] end
def conf_set( **opts) call opts.update( command: 'conf-set') end
def conf_unset( **opts) call opts.update( command: 'conf-set') end
def zone_set( **opts) call opts.update( command: 'zone-set') end
def zone_unset( **opts) call opts.update( command: 'zone-unset') end
def zone_get( **opts) call opts.update( command: 'zone-get') end
end

3
lib/knot/version.rb Normal file
View File

@ -0,0 +1,3 @@
module Knot
VERSION = "0.1.0"
end