matchd 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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