matchd 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +13 -0
  3. data/.rspec +3 -0
  4. data/.rubocop-https---relaxed-ruby-style-rubocop-yml +166 -0
  5. data/.rubocop.yml +20 -0
  6. data/.travis.yml +18 -0
  7. data/DEVELOPMENT.md +5 -0
  8. data/Gemfile +8 -0
  9. data/License.txt +373 -0
  10. data/README.md +473 -0
  11. data/Rakefile +6 -0
  12. data/bin/console +14 -0
  13. data/bin/matchd +4 -0
  14. data/bin/rake +29 -0
  15. data/bin/rspec +29 -0
  16. data/bin/setup +8 -0
  17. data/examples/registry.yml +30 -0
  18. data/exe/matchd +6 -0
  19. data/lib/matchd.rb +31 -0
  20. data/lib/matchd/cli.rb +10 -0
  21. data/lib/matchd/cli/config.rb +36 -0
  22. data/lib/matchd/cli/config_file_option.rb +23 -0
  23. data/lib/matchd/cli/main.rb +45 -0
  24. data/lib/matchd/config.rb +39 -0
  25. data/lib/matchd/control.rb +56 -0
  26. data/lib/matchd/glue.rb +6 -0
  27. data/lib/matchd/glue/async_endpoint.rb +93 -0
  28. data/lib/matchd/helpers.rb +14 -0
  29. data/lib/matchd/registry.rb +66 -0
  30. data/lib/matchd/response.rb +72 -0
  31. data/lib/matchd/response/a.rb +12 -0
  32. data/lib/matchd/response/aaaa.rb +5 -0
  33. data/lib/matchd/response/cname.rb +12 -0
  34. data/lib/matchd/response/mx.rb +16 -0
  35. data/lib/matchd/response/ns.rb +12 -0
  36. data/lib/matchd/response/ptr.rb +12 -0
  37. data/lib/matchd/response/soa.rb +28 -0
  38. data/lib/matchd/response/srv.rb +20 -0
  39. data/lib/matchd/response/txt.rb +12 -0
  40. data/lib/matchd/rule.rb +116 -0
  41. data/lib/matchd/rule/append.rb +23 -0
  42. data/lib/matchd/rule/fail.rb +18 -0
  43. data/lib/matchd/rule/invalid.rb +24 -0
  44. data/lib/matchd/rule/passthrough.rb +50 -0
  45. data/lib/matchd/rule/respond.rb +16 -0
  46. data/lib/matchd/server.rb +44 -0
  47. data/lib/matchd/version.rb +3 -0
  48. data/matchd.gemspec +31 -0
  49. metadata +191 -0
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task default: :spec
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "matchd"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ load File.expand_path("../../exe/matchd", __FILE__)
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'rake' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ require "pathname"
12
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
13
+ Pathname.new(__FILE__).realpath)
14
+
15
+ bundle_binstub = File.expand_path("../bundle", __FILE__)
16
+
17
+ if File.file?(bundle_binstub)
18
+ if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
19
+ load(bundle_binstub)
20
+ else
21
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
22
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
23
+ end
24
+ end
25
+
26
+ require "rubygems"
27
+ require "bundler/setup"
28
+
29
+ load Gem.bin_path("rake", "rake")
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'rspec' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ require "pathname"
12
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
13
+ Pathname.new(__FILE__).realpath)
14
+
15
+ bundle_binstub = File.expand_path("../bundle", __FILE__)
16
+
17
+ if File.file?(bundle_binstub)
18
+ if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
19
+ load(bundle_binstub)
20
+ else
21
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
22
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
23
+ end
24
+ end
25
+
26
+ require "rubygems"
27
+ require "bundler/setup"
28
+
29
+ load Gem.bin_path("rspec-core", "rspec")
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,30 @@
1
+ ---
2
+ version: 1
3
+ rules:
4
+ # # exact match:
5
+ # - match: mydomain.test
6
+ # resource_class: A
7
+ # respond: "10.0.0.80"
8
+ #
9
+ # # using ruby regular expressions to match any sub-domains
10
+ # - match: /^(\w+\.)*mydomain\.test$/
11
+ # resource_class: A
12
+ # respond: "10.0.0.80"
13
+ #
14
+ # # IPv6 records
15
+ # - match: /^(\w+\.)*mydomain\.test$/
16
+ # resource_class: AAAA
17
+ # respond: "fe80::A:0:0:0050"
18
+ #
19
+ # # MX records
20
+ # - match: mydomain.test
21
+ # resource_class: MX
22
+ # respond:
23
+ # preference: 10
24
+ # host: 'mail.mydomain.test.'
25
+ #
26
+ # - match: mydomain.test
27
+ # resource_class: NS
28
+ # respond:
29
+ # - host: 'ns1.mydomain.test.'
30
+ # - host: 'ns2.mydomain.test.'
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ require "pp"
3
+ require "rubygems"
4
+ require "matchd/cli"
5
+
6
+ Matchd::CLI::Main.start(ARGV)
@@ -0,0 +1,31 @@
1
+ require "matchd/version"
2
+
3
+ module Matchd
4
+ autoload :Config, "matchd/config"
5
+ autoload :Control, "matchd/control"
6
+ autoload :Glue, "matchd/glue"
7
+ autoload :Helpers, "matchd/helpers"
8
+ autoload :Registry, "matchd/registry"
9
+ autoload :Response, "matchd/response"
10
+ autoload :Rule, "matchd/rule"
11
+ autoload :Server, "matchd/server"
12
+
13
+ def self.configure(&block)
14
+ Config.configure(&block)
15
+ end
16
+
17
+ def self.configure_from_file!(config_file = Config::DEFAULT_CONFIG_FILE)
18
+ Config.configure do |config|
19
+ YAML.load_file(config_file).each do |k, v|
20
+ config.public_send("#{k}=", v)
21
+ end
22
+ end
23
+ end
24
+
25
+ def self.root
26
+ File.expand_path('..', __dir__)
27
+ end
28
+
29
+ extend Response::Factory
30
+ extend Rule::Factory
31
+ end
@@ -0,0 +1,10 @@
1
+ require "thor"
2
+
3
+ module Matchd
4
+ module CLI
5
+ autoload :ConfigFileOption, "matchd/cli/config_file_option"
6
+
7
+ autoload :Config, "matchd/cli/config"
8
+ autoload :Main, "matchd/cli/main"
9
+ end
10
+ end
@@ -0,0 +1,36 @@
1
+ module Matchd::CLI
2
+ class Config < Thor
3
+ include Thor::Actions
4
+
5
+ add_runtime_options!
6
+ class_option :verbose, type: :boolean, aliases: "-v", group: :runtime,
7
+ desc: "Print out additional logging information"
8
+
9
+ desc "setup [options]", "Creates the basic configuration files"
10
+ option :base,
11
+ aliases: "-b",
12
+ type: :string,
13
+ default: Matchd::Config::DEFAULT_DOT_DIR,
14
+ desc: "the base directory for all config files"
15
+ option :config_file,
16
+ aliases: "-C",
17
+ type: :string,
18
+ default: Matchd::Config::DEFAULT_CONFIG_FILE,
19
+ desc: "Name of the config file to create. Relative to 'base' or absolute path"
20
+ def setup
21
+ opts = options.dup
22
+
23
+ dot_dir = File.expand_path(opts.delete(:base))
24
+ config_file = File.expand_path(opts.delete(:config_file), dot_dir)
25
+
26
+ Matchd.configure { |c| c.dot_dir = dot_dir }
27
+
28
+ empty_directory(dot_dir, opts)
29
+
30
+ create_file(config_file, YAML.dump(Matchd::Config.config.to_h), opts)
31
+
32
+ sample_registry = File.expand_path(File.join("examples", "registry.yml"), Matchd.root)
33
+ create_file(Matchd::Config.registry_file, File.binread(sample_registry), opts)
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,23 @@
1
+ module Matchd::CLI
2
+ # A little patch to allow a "class_option" for letting Matchd be configured
3
+ # using the given or default config file
4
+ module ConfigFileOption
5
+ def self.included(receiver)
6
+ receiver.class_exec do
7
+ class_option :config,
8
+ type: :string,
9
+ aliases: "-c",
10
+ group: :runtime,
11
+ default: Matchd::Config::DEFAULT_CONFIG_FILE,
12
+ desc: "The config file to read"
13
+
14
+ no_commands do
15
+ def initialize(args = [], local_options = {}, config = {})
16
+ super
17
+ Matchd.configure_from_file!(options[:config]) if File.file?(options[:config])
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,45 @@
1
+ require "matchd"
2
+
3
+ module Matchd::CLI
4
+ class Main < Thor
5
+ package_name "Matchd"
6
+
7
+ include ConfigFileOption
8
+
9
+ desc "start [options]", "Start the matchd dns service"
10
+ option :deamonize,
11
+ default: true,
12
+ type: :boolean,
13
+ desc: "Start as background daemon."
14
+ def start
15
+ Matchd::Control.new.start(ontop: !options[:deamonize])
16
+ end
17
+
18
+ desc "stop", "Stop the running matchd dns daemon"
19
+ def stop
20
+ Matchd::Control.new.stop
21
+ end
22
+
23
+ desc "status", "Print process status information"
24
+ def status
25
+ Matchd::Control.new.status
26
+ end
27
+
28
+ desc "restart", "Restart the running matchd daemon"
29
+ long_desc "Stop and Start with new options. " \
30
+ "This is the same as running stop and start successively.\n\n" \
31
+ "If your configuration changes the 'dot_dir' you'll need to stop using the old config and start with the new one."
32
+ option :deamonize,
33
+ default: true,
34
+ type: :boolean,
35
+ desc: "Restart as background daemon.",
36
+ long_desc: ""
37
+ def restart
38
+ invoke :stop
39
+ invoke :start
40
+ end
41
+
42
+ desc "config SUBCOMMAND ...ARGS", "manage configuration files"
43
+ subcommand "config", Config
44
+ end
45
+ end
@@ -0,0 +1,39 @@
1
+ require "yaml"
2
+ require "dry-configurable"
3
+
4
+ module Matchd
5
+ module Config
6
+ DEFAULT_PORT = 15353
7
+ DEFAULT_DOT_DIR = File.join(ENV["HOME"], ".matchd")
8
+ DEFAULT_CONFIG_FILE = File.join(DEFAULT_DOT_DIR, "config.yml")
9
+
10
+ extend Dry::Configurable
11
+
12
+ # Base directory for config and daemon files
13
+ setting(:dot_dir, DEFAULT_DOT_DIR, reader: true) { |f| File.expand_path(f) }
14
+
15
+ # where the server will listen on
16
+ setting(
17
+ :listen,
18
+ [
19
+ { "protocol" => "udp", "ip" => "127.0.0.1", "port" => DEFAULT_PORT },
20
+ { "protocol" => "udp", "ip" => "::1", "port" => DEFAULT_PORT }
21
+ ],
22
+ reader: true
23
+ )
24
+
25
+ # The upstream resolver for passthrough requests.
26
+ # "system" will try to read your systems DNS setup.
27
+ # Give a specific config like this:
28
+ #
29
+ # [{"protocol" => "udp", "ip" => "1.1.1.1", "port" => 53},
30
+ # {"protocol" => "tcp", "ip" => "1.1.1.1", "port" => 53}]
31
+ #
32
+ setting(:resolver, "system", reader: true)
33
+
34
+ setting(:registry_file, "registry.yml")
35
+ def self.registry_file
36
+ File.expand_path(config.registry_file, dot_dir)
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,56 @@
1
+ require "daemons"
2
+
3
+ module Matchd
4
+ # Controls the demonizing of a {Matchd::Server}
5
+ class Control
6
+ def initialize(options = {})
7
+ @name = options.delete(:name, "matchd")
8
+ end
9
+
10
+ def start(options = {})
11
+ run! "start", ontop: options.fetch(:ontop, false)
12
+ end
13
+
14
+ def stop
15
+ run! "stop"
16
+ end
17
+
18
+ def status
19
+ run! "status"
20
+ end
21
+
22
+ def restart(options = {})
23
+ stop
24
+ start(options)
25
+ end
26
+
27
+ # @private
28
+ def run!(argv, options = {})
29
+ run_options = { ARGV: Array(argv), **options, **daemon_opts }
30
+ Daemons.run_proc(@name, run_options) do
31
+ require "matchd/server"
32
+ Matchd::Server.new(*server_opts).run
33
+ end
34
+ end
35
+
36
+ # @private
37
+ def server_opts
38
+ [
39
+ Matchd::Registry.load_file(Matchd::Config.registry_file),
40
+ Matchd::Config.listen,
41
+ { resolver: Matchd::Config.resolver }
42
+ ]
43
+ end
44
+
45
+ # @private
46
+ def daemon_opts
47
+ daemon_dir = Matchd::Config.dot_dir
48
+ {
49
+ dir_mode: :normal,
50
+ dir: daemon_dir,
51
+ log_output: true,
52
+ log_dir: daemon_dir
53
+ }
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,6 @@
1
+ module Matchd
2
+ # Wrappers for dealing with other lib's data structures
3
+ module Glue
4
+ autoload :AsyncEndpoint, "matchd/glue/async_endpoint"
5
+ end
6
+ end
@@ -0,0 +1,93 @@
1
+ module Matchd::Glue
2
+ # Wrapper for allowing a more flexible way of defining interfaces for Asyc-*
3
+ module AsyncEndpoint
4
+ class << self
5
+ # Examples:
6
+ #
7
+ # # classic triplet array
8
+ # AsyncIntrface.parse([:udp, "0.0.0.0", 53])
9
+ # AsyncIntrface.parse([[:udp, "0.0.0.0", 53], [:tcp, "0.0.0.0", 53]])
10
+ #
11
+ # # Hash notation
12
+ # AsyncIntrface.parse({"protocol" => :udp, "ip" => "0.0.0.0", "port" => 53})
13
+ # AsyncIntrface.parse({protocol: :udp, ip: "0.0.0.0", port: 53})
14
+ # AsyncIntrface.parse([{protocol: :udp, ip: "0.0.0.0", port: 53}, {protocol: :tcp, ip: "0.0.0.0", port: 53}])
15
+ #
16
+ # # URI strings
17
+ # AsyncIntrface.parse("udp://0.0.0.0:53")
18
+ # AsyncIntrface.parse(["udp://0.0.0.0:53", "tcp://0.0.0.0:53"])
19
+ def parse(args)
20
+ val =
21
+ case args
22
+ when Array
23
+ if triplet = parse_array(args)
24
+ [triplet]
25
+ else
26
+ args.flat_map { |e| parse(e) }
27
+ end
28
+ when Hash
29
+ [parse_hash(args)]
30
+ when String
31
+ [parse_uri(args)]
32
+ end
33
+
34
+ return nil unless val
35
+
36
+ val.compact!
37
+
38
+ val.empty? ? nil : val
39
+ end
40
+
41
+ # Supported DNS protocols
42
+ PROTOCOLS = %w(udp tcp).freeze
43
+
44
+ # Default port used when the port is omitted in Hash or String notation.
45
+ DEFAULT_PORT = 53
46
+
47
+ private
48
+
49
+ # @private
50
+ # parses the classic triplet array notation
51
+ # `[:udp, "0.0.0.0", 53]`
52
+ # and returns just the thing if all data is present.
53
+ # To ensure proper triplet detection, all values are required. No port default.
54
+ def parse_array(args)
55
+ protocol, ip, port = args
56
+
57
+ return nil unless PROTOCOLS.include?(protocol.to_s) && ip && port
58
+
59
+ [protocol.to_sym, ip, port]
60
+ end
61
+
62
+ # @private
63
+ # parses a single Hash with named components in string or symbol keys
64
+ # `{"protocol" => :udp, "ip" => "0.0.0.0", "port" => 53}` or
65
+ # `{protocol: :udp, ip: "0.0.0.0", port: 53}`
66
+ # and returns it's triplet notation if all parts are present.
67
+ # there's no additional array wrapping.
68
+ def parse_hash(args)
69
+ protocol = args["protocol"] || args[:protocol]
70
+ ip = args["ip"] || args[:ip]
71
+ port = args["port"] || args[:port] || DEFAULT_PORT
72
+
73
+ return nil unless PROTOCOLS.include?(protocol.to_s) && ip
74
+
75
+ [protocol.to_sym, ip, port]
76
+ end
77
+
78
+ # @private
79
+ # parses a URI string
80
+ # `"udp://0.0.0.0:53"` or `"tcp://0.0.0.0:53"`
81
+ def parse_uri(args)
82
+ uri = URI.parse(args)
83
+ protocol = uri.scheme
84
+ ip = uri.host
85
+ port = uri.port || DEFAULT_PORT
86
+
87
+ return nil unless PROTOCOLS.include?(protocol.to_s) && ip
88
+
89
+ [protocol.to_sym, ip, port]
90
+ end
91
+ end
92
+ end
93
+ end