devdnsd 1.0.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.
- data/.DS_Store +0 -0
- data/.gitignore +21 -0
- data/.yardopts +1 -0
- data/Gemfile +9 -0
- data/Gemfile.lock +93 -0
- data/README.md +79 -0
- data/Rakefile +18 -0
- data/bin/devdnsd +77 -0
- data/config/devdnsd_config.sample +24 -0
- data/devdnsd.gemspec +40 -0
- data/lib/devdnsd.rb +23 -0
- data/lib/devdnsd/application.rb +366 -0
- data/lib/devdnsd/configuration.rb +94 -0
- data/lib/devdnsd/errors.rb +18 -0
- data/lib/devdnsd/logger.rb +84 -0
- data/lib/devdnsd/rule.rb +130 -0
- data/lib/devdnsd/version.rb +23 -0
- data/spec/coverage_helper.rb +19 -0
- data/spec/devdnsd/application_spec.rb +436 -0
- data/spec/devdnsd/configuration_spec.rb +77 -0
- data/spec/devdnsd/logger_spec.rb +86 -0
- data/spec/devdnsd/rule_spec.rb +111 -0
- data/spec/spec_helper.rb +13 -0
- data/utils/tester.rb +127 -0
- metadata +269 -0
@@ -0,0 +1,94 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
#
|
3
|
+
# This file is part of the devdns gem. Copyright (C) 2012 and above Shogun <shogun_panda@me.com>.
|
4
|
+
# Licensed under the MIT license, which can be found at http://www.opensource.org/licenses/mit-license.php.
|
5
|
+
#
|
6
|
+
|
7
|
+
module DevDNSd
|
8
|
+
# This class holds the configuration of the applicaton.
|
9
|
+
class Configuration
|
10
|
+
# If to run the server in foreground. Default: `false`.
|
11
|
+
attr_accessor :foreground
|
12
|
+
|
13
|
+
# The address to listen to. Default: `0.0.0.0`.
|
14
|
+
attr_accessor :address
|
15
|
+
|
16
|
+
# The port to listen to. Default: `7771`.
|
17
|
+
attr_accessor :port
|
18
|
+
|
19
|
+
# The TLD to manage. Default: `dev`.
|
20
|
+
attr_accessor :tld
|
21
|
+
|
22
|
+
# The file to log to. Default: `/var/log/devdnsd.log`.
|
23
|
+
attr_accessor :log_file
|
24
|
+
|
25
|
+
# The minimum severity to log. Default: `Logger::INFO`.
|
26
|
+
attr_accessor :log_level
|
27
|
+
|
28
|
+
# The rules of the server. By default, every hostname is resolved with `127.0.0.1`.
|
29
|
+
attr_accessor :rules
|
30
|
+
|
31
|
+
# Creates a new configuration.
|
32
|
+
# A configuration file is a plain Ruby file with a top-level {Configuration config} object.
|
33
|
+
#
|
34
|
+
# Example:
|
35
|
+
#
|
36
|
+
# ```ruby
|
37
|
+
# config.add_rule("match.dev", "10.0.0.1")
|
38
|
+
# ```
|
39
|
+
#
|
40
|
+
# @param file [String] The file to read.
|
41
|
+
# @param application [Application] The application which this configuration is attached to.
|
42
|
+
# @param overrides [Hash] A set of values which override those set in the configuration file.
|
43
|
+
def initialize(file = nil, application = nil, overrides = {})
|
44
|
+
@address = "0.0.0.0"
|
45
|
+
@port = 7771
|
46
|
+
@tld = "dev"
|
47
|
+
@log_file = "/var/log/devdnsd.log"
|
48
|
+
@log_level = Logger::INFO
|
49
|
+
@rules = []
|
50
|
+
@foreground = false
|
51
|
+
|
52
|
+
if file.present?
|
53
|
+
begin
|
54
|
+
# Open the file
|
55
|
+
path = Pathname.new(file).realpath
|
56
|
+
application.logger.info("Using configuration file #{path}.") if application
|
57
|
+
self.tap do |config|
|
58
|
+
eval(File.read(path))
|
59
|
+
end
|
60
|
+
|
61
|
+
@log_file = $stdout if @log_file == "STDOUT"
|
62
|
+
@log_file = $stderr if @log_file == "STDERR"
|
63
|
+
rescue Errno::ENOENT, LoadError
|
64
|
+
rescue Exception => e
|
65
|
+
raise DevDNSd::Errors::InvalidConfiguration.new("Config file #{file} is not valid.")
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# Apply overrides
|
70
|
+
if overrides.is_a?(Hash) then
|
71
|
+
overrides.each_pair do |k, v|
|
72
|
+
self.send("#{k}=", v) if self.respond_to?("#{k}=") && !v.nil?
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
# Make sure some arguments are of correct type
|
77
|
+
@port = @port.to_integer
|
78
|
+
@log_level = @log_level.to_integer
|
79
|
+
|
80
|
+
# Add a default rule
|
81
|
+
self.add_rule(/.+/, "127.0.0.1") if @rules.length == 0
|
82
|
+
end
|
83
|
+
|
84
|
+
# Adds a rule to the configuration.
|
85
|
+
#
|
86
|
+
# @param args [Array] The rule's arguments.
|
87
|
+
# @param block [Proc] An optional block for the rule.
|
88
|
+
# @return [Array] The current set of rule.
|
89
|
+
# @see Rule.create
|
90
|
+
def add_rule(*args, &block)
|
91
|
+
@rules << DevDNSd::Rule.create(*args, &block)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
#
|
3
|
+
# This file is part of the devdns gem. Copyright (C) 2012 and above Shogun <shogun_panda@me.com>.
|
4
|
+
# Licensed under the MIT license, which can be found at http://www.opensource.org/licenses/mit-license.php.
|
5
|
+
#
|
6
|
+
|
7
|
+
module DevDNSd
|
8
|
+
# Exceptions for {DevDNSd DevDNSd}.
|
9
|
+
module Errors
|
10
|
+
# This exception is raised if a {Rule Rule} is invalid.
|
11
|
+
class InvalidRule < ArgumentError
|
12
|
+
end
|
13
|
+
|
14
|
+
# This exception is raised if a {Configuration Configuration} is invalid.
|
15
|
+
class InvalidConfiguration < ArgumentError
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
#
|
3
|
+
# This file is part of the devdnsd gem. Copyright (C) 2012 and above Shogun <shogun_panda@me.com>.
|
4
|
+
# Licensed under the MIT license, which can be found at http://www.opensource.org/licenses/mit-license.php.
|
5
|
+
#
|
6
|
+
|
7
|
+
module DevDNSd
|
8
|
+
# A custom logger for DevDNSd.
|
9
|
+
class Logger < ::Logger
|
10
|
+
# The start time of first line. This allows to show a `T+0.1234` information into the log.
|
11
|
+
mattr_accessor :start_time
|
12
|
+
|
13
|
+
# The file or device to log messages to.
|
14
|
+
attr_reader :device
|
15
|
+
|
16
|
+
# Creates a new logger
|
17
|
+
# @see http://www.ruby-doc.org/stdlib-1.9.3/libdoc/logger/rdoc/Logger.html
|
18
|
+
#
|
19
|
+
# @param logdev [String|IO] The log device. This is a filename (String) or IO object (typically STDOUT, STDERR, or an open file).
|
20
|
+
# @param shift_age [Fixnum] Number of old log files to keep, or frequency of rotation (daily, weekly or monthly).
|
21
|
+
# @param shift_size [Fixnum] Maximum logfile size (only applies when shift_age is a number).
|
22
|
+
def initialize(logdev, shift_age = 0, shift_size = 1048576)
|
23
|
+
@device = logdev
|
24
|
+
super(logdev, shift_age, shift_size)
|
25
|
+
end
|
26
|
+
|
27
|
+
# Creates a new logger
|
28
|
+
#
|
29
|
+
# @param file [String|IO] The log device. This is a filename (String) or IO object (typically STDOUT, STDERR, or an open file).
|
30
|
+
# @param level [Fixnum] The minimum severity to log. See http://www.ruby-doc.org/stdlib-1.9.3/libdoc/logger/rdoc/Logger.html for valid levels.
|
31
|
+
# @param formatter [Proc] The formatter to use for logging.
|
32
|
+
# @return [Logger] The new logger.
|
33
|
+
def self.create(file = nil, level = Logger::INFO, formatter = nil)
|
34
|
+
file ||= self.default_file
|
35
|
+
rv = self.new(self.get_real_file(file))
|
36
|
+
rv.level = level.to_i
|
37
|
+
rv.formatter = formatter || self.default_formatter
|
38
|
+
rv
|
39
|
+
end
|
40
|
+
|
41
|
+
# Translates a file to standard input or standard ouput in some special cases.
|
42
|
+
#
|
43
|
+
# @param file [String] The string to translate.
|
44
|
+
# @return [String|IO] The translated file name.
|
45
|
+
def self.get_real_file(file)
|
46
|
+
case file
|
47
|
+
when "STDOUT" then $stdout
|
48
|
+
when "STDERR" then $stderr
|
49
|
+
else file
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# The default file for logging.
|
54
|
+
# @return [String|IO] The default file for logging.
|
55
|
+
def self.default_file
|
56
|
+
@default_file ||= $stdout
|
57
|
+
end
|
58
|
+
|
59
|
+
# The default formatter for logging.
|
60
|
+
# @return [Proc] The default formatter for logging.
|
61
|
+
def self.default_formatter
|
62
|
+
@default_formatter ||= Proc.new {|severity, datetime, progname, msg|
|
63
|
+
color = case severity
|
64
|
+
when "DEBUG" then :cyan
|
65
|
+
when "INFO" then :green
|
66
|
+
when "WARN" then :yellow
|
67
|
+
when "ERROR" then :red
|
68
|
+
when "FATAL" then :magenta
|
69
|
+
else :white
|
70
|
+
end
|
71
|
+
|
72
|
+
header = ("[%s T+%0.5f] %s:" %[datetime.strftime("%Y/%b/%d %H:%M:%S"), [datetime.to_f - self.start_time.to_f, 0].max, severity.rjust(5)]).bright
|
73
|
+
header = header.color(color) if color.present?
|
74
|
+
"%s %s\n" % [header, msg]
|
75
|
+
}
|
76
|
+
end
|
77
|
+
|
78
|
+
# The log time of the first logger. This allows to show a `T+0.1234` information into the log.
|
79
|
+
# @return [Time] The log time of the first logger.
|
80
|
+
def self.start_time
|
81
|
+
@start_time ||= Time.now
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
data/lib/devdnsd/rule.rb
ADDED
@@ -0,0 +1,130 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
#
|
3
|
+
# This file is part of the devdns gem. Copyright (C) 2012 and above Shogun <shogun_panda@me.com>.
|
4
|
+
# Licensed under the MIT license, which can be found at http://www.opensource.org/licenses/mit-license.php.
|
5
|
+
#
|
6
|
+
|
7
|
+
module DevDNSd
|
8
|
+
# This class encapsulate a rule for matching an hostname.
|
9
|
+
class Rule
|
10
|
+
# The pattern to match. Default: `/.+/`.
|
11
|
+
attr_accessor :match
|
12
|
+
|
13
|
+
# The type of request to match. Default: `:A`.
|
14
|
+
#
|
15
|
+
# @see .create
|
16
|
+
attr_accessor :type
|
17
|
+
|
18
|
+
# The IP or hostname to reply back to the client. Default: `127.0.0.1`.
|
19
|
+
#
|
20
|
+
# @see .create
|
21
|
+
attr_accessor :reply
|
22
|
+
|
23
|
+
# A list of options for the request. Default is an empty hash.
|
24
|
+
attr_accessor :options
|
25
|
+
|
26
|
+
# An optional block to compute the reply instead of using the `reply` parameter.
|
27
|
+
#
|
28
|
+
# @see .create
|
29
|
+
attr_accessor :block
|
30
|
+
|
31
|
+
# Creates a new rule.
|
32
|
+
#
|
33
|
+
# @param match [String|Regexp] The pattern to match.
|
34
|
+
# @param reply [String] The IP or hostname to reply back to the client.
|
35
|
+
# @param type [Symbol] The type of request to match.
|
36
|
+
# @param options [Hash] A list of options for the request.
|
37
|
+
# @param block [Proc] An optional block to compute the reply instead of using the `reply` parameter.
|
38
|
+
# @see .create
|
39
|
+
def initialize(match = /.+/, reply = "127.0.0.1", type = :A, options = {}, &block)
|
40
|
+
reply ||= "127.0.0.1"
|
41
|
+
type ||= :A
|
42
|
+
@match = match
|
43
|
+
@type = type
|
44
|
+
@reply = block.blank? ? reply : nil
|
45
|
+
@options = options
|
46
|
+
@block = block
|
47
|
+
|
48
|
+
raise(DevDNSd::Errors::InvalidRule.new("You must specify at least a rule and a host (also via a block). Optionally you can add a record type (default: A) and the options.")) if @reply.blank? && @block.nil?
|
49
|
+
raise(DevDNSd::Errors::InvalidRule.new("You can only use hashs for options.")) if !@options.is_a?(Hash)
|
50
|
+
end
|
51
|
+
|
52
|
+
# Returns the resource class(es) for the current rule.
|
53
|
+
#
|
54
|
+
# @return [Array|Class] The class(es) for the current rule.
|
55
|
+
def resource_class
|
56
|
+
classes = self.type.ensure_array.collect {|cls| self.class.symbol_to_resource_class(cls) }.compact.uniq
|
57
|
+
classes.length == 1 ? classes.first : classes
|
58
|
+
end
|
59
|
+
|
60
|
+
# Checks if the rule is a regexp.
|
61
|
+
#
|
62
|
+
# @return [Boolean] `true` if the rule is a Regexp, `false` otherwise.
|
63
|
+
def is_regexp?
|
64
|
+
self.match.is_a?(Regexp)
|
65
|
+
end
|
66
|
+
|
67
|
+
# Checks if the rule is a regexp.
|
68
|
+
#
|
69
|
+
# @return [Boolean] `true` if the rule has a block, `false` otherwise.
|
70
|
+
def has_block?
|
71
|
+
self.block.present?
|
72
|
+
end
|
73
|
+
|
74
|
+
# Matches a hostname to the rule.
|
75
|
+
#
|
76
|
+
# @param [String] The hostname to match.
|
77
|
+
# @return [MatchData|Boolean|Nil] Return `true` or MatchData (if the pattern is a regexp) if the rule matches, `false` or `nil` otherwise.
|
78
|
+
def match_host(hostname)
|
79
|
+
self.is_regexp? ? self.match.match(hostname) : (self.match == hostname)
|
80
|
+
end
|
81
|
+
|
82
|
+
# Creates a new rule.
|
83
|
+
#
|
84
|
+
# @param match [String|Regexp] The pattern to match.
|
85
|
+
# @param reply_or_type [String|Symbol] The IP or hostname to reply back to the client (or the type of request to match, if a block is provided).
|
86
|
+
# @param type [Symbol] The type of request to match. This is ignored if a block is provided.
|
87
|
+
# @param options [Hash] A list of options for the request.
|
88
|
+
# @param block [Proc] An optional block to compute the reply instead of using the `reply_or_type` parameter. In this case `reply_or_type` is used for the type of the request and `type` is ignored.
|
89
|
+
def self.create(match, reply_or_type = nil, type = nil, options = {}, &block)
|
90
|
+
raise(DevDNSd::Errors::InvalidRule.new("You must specify at least a rule and a host (also via a block). Optionally you can add a record type (default: A) and the options.")) if reply_or_type.blank? && block.nil?
|
91
|
+
raise(DevDNSd::Errors::InvalidRule.new("You can only use hashs for options.")) if !options.is_a?(Hash)
|
92
|
+
|
93
|
+
rv = self.new(match)
|
94
|
+
rv.options = options
|
95
|
+
|
96
|
+
if block.present? then # reply_or_type acts like a type, type is ignored
|
97
|
+
rv.type = reply_or_type || :A
|
98
|
+
rv.reply = nil
|
99
|
+
rv.block = block
|
100
|
+
else # reply_or_type acts like a reply
|
101
|
+
rv.reply = reply_or_type || "127.0.0.1"
|
102
|
+
rv.type = type || :A
|
103
|
+
end
|
104
|
+
|
105
|
+
rv
|
106
|
+
end
|
107
|
+
|
108
|
+
# Converts a class to the correspondent symbol.
|
109
|
+
#
|
110
|
+
# @param klass [Class] The class to convert.
|
111
|
+
# @return [Symbol] The symbol representation of the class.
|
112
|
+
def self.resource_class_to_symbol(klass)
|
113
|
+
klass.to_s.gsub(/(.+::)?(.+)/, "\\2").to_sym
|
114
|
+
end
|
115
|
+
|
116
|
+
# Converts a symbol to the correspondent DNS resource class.
|
117
|
+
#
|
118
|
+
# @param symbol [Symbol] The symbol to convert.
|
119
|
+
# @return [Symbol] The class associated to the symbol.
|
120
|
+
def self.symbol_to_resource_class(symbol)
|
121
|
+
symbol = symbol.to_s.upcase
|
122
|
+
|
123
|
+
begin
|
124
|
+
"Resolv::DNS::Resource::IN::#{symbol}".constantize
|
125
|
+
rescue NameError
|
126
|
+
raise(DevDNSd::Errors::InvalidRule.new("Invalid resource class #{symbol}."))
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
#
|
3
|
+
# This file is part of the devdns gem. Copyright (C) 2012 and above Shogun <shogun_panda@me.com>.
|
4
|
+
# Licensed under the MIT license, which can be found at http://www.opensource.org/licenses/mit-license.php.
|
5
|
+
#
|
6
|
+
|
7
|
+
module DevDNSd
|
8
|
+
# The current version of DevDNSd.
|
9
|
+
# For version, semantic version is used. See: http://semver.org/
|
10
|
+
module Version
|
11
|
+
# The major version.
|
12
|
+
MAJOR = 1
|
13
|
+
|
14
|
+
# The minor version.
|
15
|
+
MINOR = 0
|
16
|
+
|
17
|
+
# The patch version.
|
18
|
+
PATCH = 0
|
19
|
+
|
20
|
+
# The current version number of DevDNSd.
|
21
|
+
STRING = [MAJOR, MINOR, PATCH].compact.join(".")
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
#
|
3
|
+
# This file is part of the devdns gem. Copyright (C) 2012 and above Shogun <shogun_panda@me.com>.
|
4
|
+
# Licensed under the MIT license, which can be found at http://www.opensource.org/licenses/mit-license.php.
|
5
|
+
#
|
6
|
+
|
7
|
+
require "simplecov"
|
8
|
+
require "pathname"
|
9
|
+
|
10
|
+
if ENV["DEVDNSD_COVERAGE"] == "TRUE" then
|
11
|
+
root = Pathname.new(File.dirname(__FILE__)) + ".."
|
12
|
+
|
13
|
+
SimpleCov.start do
|
14
|
+
add_filter do |src_file|
|
15
|
+
path = Pathname.new(src_file.filename).relative_path_from(root).to_s
|
16
|
+
path =~ /^lib\/devdnsd\/patches/ || path !~ /^(bin|lib)/
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,436 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
#
|
3
|
+
# This file is part of the devdns gem. Copyright (C) 2012 and above Shogun <shogun_panda@me.com>.
|
4
|
+
# Licensed under the MIT license, which can be found at http://www.opensource.org/licenses/mit-license.php.
|
5
|
+
#
|
6
|
+
|
7
|
+
require "spec_helper"
|
8
|
+
|
9
|
+
describe DevDNSd::Application do
|
10
|
+
before(:each) do
|
11
|
+
DevDNSd::Logger.stub(:default_file).and_return("/dev/null")
|
12
|
+
end
|
13
|
+
|
14
|
+
let(:application){ app = DevDNSd::Application.instance }
|
15
|
+
let(:executable) { Pathname.new(File.dirname((__FILE__))) + "../../bin/devdnsd" }
|
16
|
+
let(:sample_config) { Pathname.new(File.dirname((__FILE__))) + "../../config/devdnsd_config.sample" }
|
17
|
+
let(:resolver_path) { "/tmp/devdnsd-test-resolver-#{Time.now.strftime("%Y%m%d-%H:%M:%S")}" }
|
18
|
+
let(:launch_agent_path) { "/tmp/devdnsd-test-agent-#{Time.now.strftime("%Y%m%d-%H:%M:%S")}" }
|
19
|
+
|
20
|
+
describe "#initialize" do
|
21
|
+
it("should setup the logger") do application.logger.should_not be_nil end
|
22
|
+
it("should setup the configuration") do application.config.should_not be_nil end
|
23
|
+
|
24
|
+
it("should abort with an invalid configuration") do
|
25
|
+
path = "/tmp/devdnsd-test-#{Time.now.strftime("%Y%m%d-%H:%M:%S")}"
|
26
|
+
file = File.new(path, "w")
|
27
|
+
file.write("config.port = ")
|
28
|
+
file.close
|
29
|
+
|
30
|
+
expect { DevDNSd::Application.new({:config => file.path}) }.to raise_error(SystemExit)
|
31
|
+
File.unlink(path)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
describe "#run" do
|
36
|
+
it "should run the server" do
|
37
|
+
application.should_receive(:perform_server)
|
38
|
+
|
39
|
+
Thread.start {
|
40
|
+
devdnsd_resolv("match.dev")
|
41
|
+
DevDNSd::Application.quit
|
42
|
+
}
|
43
|
+
|
44
|
+
DevDNSd::Application.run
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
describe "#perform_server" do
|
49
|
+
before(:each) do
|
50
|
+
DevDNSd::Logger.stub(:default_file).and_return($stdout)
|
51
|
+
end
|
52
|
+
|
53
|
+
it "should run the server" do
|
54
|
+
class DevDNSd::Application
|
55
|
+
def on_start
|
56
|
+
EventMachine.add_periodic_timer(1) do
|
57
|
+
devdnsd_resolv("match.dev")
|
58
|
+
DevDNSd::Application.quit
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
RubyDNS.should_receive(:run_server)
|
64
|
+
DevDNSd::Application.instance.perform_server
|
65
|
+
end
|
66
|
+
|
67
|
+
it "should iterate the rules" do
|
68
|
+
class DevDNSd::Application
|
69
|
+
def on_start
|
70
|
+
EventMachine.add_periodic_timer(1) do
|
71
|
+
devdnsd_resolv("match.dev")
|
72
|
+
DevDNSd::Application.quit
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
DevDNSd::Application.instance.config.rules.should_receive(:each).at_least(1)
|
78
|
+
DevDNSd::Application.instance.perform_server
|
79
|
+
end
|
80
|
+
|
81
|
+
it "should call process_rule" do
|
82
|
+
class DevDNSd::Application
|
83
|
+
def on_start
|
84
|
+
EventMachine.add_periodic_timer(1) do
|
85
|
+
devdnsd_resolv("match.dev")
|
86
|
+
DevDNSd::Application.quit
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
DevDNSd::Application.instance.should_receive(:process_rule).at_least(1)
|
92
|
+
DevDNSd::Application.instance.perform_server
|
93
|
+
end
|
94
|
+
|
95
|
+
it "should complain about wrong rules" do
|
96
|
+
class DevDNSd::Application
|
97
|
+
def on_start
|
98
|
+
EventMachine.add_periodic_timer(1) do
|
99
|
+
devdnsd_resolv("invalid.dev")
|
100
|
+
DevDNSd::Application.quit
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
DevDNSd::Application.instance.stub(:process_rule).and_raise(Exception)
|
106
|
+
expect { DevDNSd::Application.instance.perform_server }.to raise_exception
|
107
|
+
end
|
108
|
+
|
109
|
+
describe "should correctly resolve hostnames" do
|
110
|
+
before(:all) do
|
111
|
+
system("ruby \"#{executable}\" -L 0 -l /dev/null -c \"#{sample_config}\" start > /dev/null 2>&1")
|
112
|
+
end
|
113
|
+
|
114
|
+
after(:all) do
|
115
|
+
system("ruby \"#{executable}\" -L 0 -l /dev/null stop > /dev/null 2>&1")
|
116
|
+
end
|
117
|
+
|
118
|
+
it "basing on a exact pattern" do
|
119
|
+
devdnsd_resolv("match_1.dev").should == ["10.0.1.1", :A]
|
120
|
+
devdnsd_resolv("match_2.dev").should == ["10.0.2.1", :MX]
|
121
|
+
devdnsd_resolv("match_3.dev").should == ["10.0.3.1", :A]
|
122
|
+
devdnsd_resolv("match_4.dev").should == ["10.0.4.1", :CNAME]
|
123
|
+
end
|
124
|
+
|
125
|
+
it "basing on a regexp pattern" do
|
126
|
+
devdnsd_resolv("match_5_11.dev").should == ["10.0.5.11", :A]
|
127
|
+
devdnsd_resolv("match_5_22.dev").should == ["10.0.5.22", :A]
|
128
|
+
devdnsd_resolv("match_6_33.dev").should == ["10.0.6.33", :PTR]
|
129
|
+
devdnsd_resolv("match_6_44.dev").should == ["10.0.6.44", :PTR]
|
130
|
+
devdnsd_resolv("match_7_55.dev").should == ["10.0.7.55", :A]
|
131
|
+
devdnsd_resolv("match_7_66.dev").should == ["10.0.7.66", :A]
|
132
|
+
devdnsd_resolv("match_8_77.dev").should == ["10.0.8.77", :PTR]
|
133
|
+
devdnsd_resolv("match_8_88.dev").should == ["10.0.8.88", :PTR]
|
134
|
+
|
135
|
+
end
|
136
|
+
|
137
|
+
it "and return multiple answsers" do devdnsd_resolv("match_10.dev").should == [["10.0.10.1", :A], ["10.0.10.2", :MX]] end
|
138
|
+
|
139
|
+
it "and reject invalid matches (with or without rules)" do
|
140
|
+
devdnsd_resolv("match_9.dev").should be_nil
|
141
|
+
devdnsd_resolv("invalid.dev").should be_nil
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
describe "#process_rule" do
|
147
|
+
class FakeTransaction
|
148
|
+
attr_reader :resource_class
|
149
|
+
|
150
|
+
def initialize(cls = Resolv::DNS::Resource::IN::ANY)
|
151
|
+
@resource_class = cls
|
152
|
+
end
|
153
|
+
|
154
|
+
def respond!(*args)
|
155
|
+
true
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
let(:application){ DevDNSd::Application.new({:config => Pathname.new(File.dirname((__FILE__))) + "../../config/devdnsd_config.sample" }) }
|
160
|
+
let(:transaction){ FakeTransaction.new }
|
161
|
+
|
162
|
+
it "should match a valid string request" do
|
163
|
+
rule = application.config.rules[0]
|
164
|
+
application.process_rule(rule, rule.resource_class, nil, transaction).should be_true
|
165
|
+
end
|
166
|
+
|
167
|
+
it "should match a valid string request with specific type" do
|
168
|
+
rule = application.config.rules[1]
|
169
|
+
application.process_rule(rule, rule.resource_class, nil, transaction).should be_true
|
170
|
+
end
|
171
|
+
|
172
|
+
it "should match a valid string request with a block" do
|
173
|
+
rule = application.config.rules[2]
|
174
|
+
application.process_rule(rule, rule.resource_class, nil, transaction).should be_true
|
175
|
+
end
|
176
|
+
|
177
|
+
it "should match a valid string request with a block" do
|
178
|
+
rule = application.config.rules[3]
|
179
|
+
application.process_rule(rule, rule.resource_class, nil, transaction).should be_true
|
180
|
+
end
|
181
|
+
|
182
|
+
|
183
|
+
it "should match a valid regexp request" do
|
184
|
+
rule = application.config.rules[4]
|
185
|
+
mo = rule.match_host("match_5_12.dev")
|
186
|
+
application.process_rule(rule, rule.resource_class, mo, transaction).should be_true
|
187
|
+
end
|
188
|
+
|
189
|
+
it "should match a valid regexp request with specific type" do
|
190
|
+
rule = application.config.rules[5]
|
191
|
+
mo = rule.match_host("match_6_34.dev")
|
192
|
+
application.process_rule(rule, rule.resource_class, mo, transaction).should be_true
|
193
|
+
end
|
194
|
+
|
195
|
+
it "should match a valid regexp request with a block" do
|
196
|
+
rule = application.config.rules[6]
|
197
|
+
mo = rule.match_host("match_7_56.dev")
|
198
|
+
application.process_rule(rule, rule.resource_class, mo, transaction).should be_true
|
199
|
+
end
|
200
|
+
|
201
|
+
it "should match a valid regexp request with a block and specific type" do
|
202
|
+
rule = application.config.rules[7]
|
203
|
+
mo = rule.match_host("match_8_78.dev")
|
204
|
+
application.process_rule(rule, rule.resource_class, mo, transaction).should be_true
|
205
|
+
end
|
206
|
+
|
207
|
+
it "should return false for a false block" do
|
208
|
+
rule = application.config.rules[8]
|
209
|
+
application.process_rule(rule, rule.resource_class, nil, transaction).should be_false
|
210
|
+
end
|
211
|
+
|
212
|
+
it "should return nil for a nil reply" do
|
213
|
+
rule = application.config.rules[0]
|
214
|
+
rule.reply = nil
|
215
|
+
application.process_rule(rule, rule.resource_class, nil, transaction).should be_nil
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
describe "#dns_update" do
|
220
|
+
it "should update the DNS cache" do
|
221
|
+
application.stub(:execute_command).and_return("EXECUTED")
|
222
|
+
application.dns_update.should == "EXECUTED"
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
describe "#resolver_path" do
|
227
|
+
it "should return the resolver file basing on the configuration" do application.resolver_path.should == "/etc/resolver/#{application.config.tld}" end
|
228
|
+
it "should return the resolver file basing on the argument" do application.resolver_path("foo").should == "/etc/resolver/foo" end
|
229
|
+
end
|
230
|
+
|
231
|
+
describe "launch_agent_path" do
|
232
|
+
it "should return the agent file with a default name" do application.launch_agent_path.should == ENV["HOME"] + "/Library/LaunchAgents/it.cowtech.devdnsd.plist" end
|
233
|
+
it "should return the agent file with a specified name" do application.launch_agent_path("foo").should == ENV["HOME"] + "/Library/LaunchAgents/foo.plist" end
|
234
|
+
end
|
235
|
+
|
236
|
+
describe "#action_start" do
|
237
|
+
it "should call perform_server in foreground" do
|
238
|
+
DevDNSd::Application.instance({}, {:foreground => true}, [], true).should_receive(:perform_server)
|
239
|
+
application.action_start
|
240
|
+
end
|
241
|
+
|
242
|
+
it "should start the daemon" do
|
243
|
+
DevDNSd::Application.instance({}, {}, [], true)
|
244
|
+
RExec::Daemon::Controller.should_receive(:start)
|
245
|
+
application.action_start
|
246
|
+
end
|
247
|
+
end
|
248
|
+
|
249
|
+
describe "#action_stop" do
|
250
|
+
it "should stop the daemon" do
|
251
|
+
RExec::Daemon::Controller.should_receive(:stop)
|
252
|
+
application.action_stop
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
256
|
+
describe "#action_install" do
|
257
|
+
it "should create the resolver" do
|
258
|
+
application.stub(:resolver_path).and_return(resolver_path)
|
259
|
+
application.stub(:launch_agent_path).and_return(launch_agent_path)
|
260
|
+
File.unlink(application.resolver_path) if File.exists?(application.resolver_path)
|
261
|
+
File.unlink(application.launch_agent_path) if File.exists?(application.launch_agent_path)
|
262
|
+
|
263
|
+
application.action_install
|
264
|
+
File.exists?(resolver_path).should be_true
|
265
|
+
|
266
|
+
File.unlink(application.resolver_path) if File.exists?(application.resolver_path)
|
267
|
+
File.unlink(application.launch_agent_path) if File.exists?(application.launch_agent_path)
|
268
|
+
end
|
269
|
+
|
270
|
+
it "should create the agent" do
|
271
|
+
application.stub(:resolver_path).and_return(resolver_path)
|
272
|
+
application.stub(:launch_agent_path).and_return(launch_agent_path)
|
273
|
+
File.unlink(application.resolver_path) if File.exists?(application.resolver_path)
|
274
|
+
File.unlink(application.launch_agent_path) if File.exists?(application.launch_agent_path)
|
275
|
+
|
276
|
+
application.stub(:resolver_path).and_return(resolver_path)
|
277
|
+
application.action_install
|
278
|
+
File.exists?(application.launch_agent_path).should be_true
|
279
|
+
|
280
|
+
File.unlink(application.resolver_path) if File.exists?(application.resolver_path)
|
281
|
+
File.unlink(application.launch_agent_path) if File.exists?(application.launch_agent_path)
|
282
|
+
end
|
283
|
+
|
284
|
+
it "should update the DNS cache" do
|
285
|
+
application.stub(:resolver_path).and_return(resolver_path)
|
286
|
+
application.stub(:launch_agent_path).and_return(launch_agent_path)
|
287
|
+
File.unlink(application.resolver_path) if File.exists?(application.resolver_path)
|
288
|
+
File.unlink(application.launch_agent_path) if File.exists?(application.launch_agent_path)
|
289
|
+
|
290
|
+
application.should_receive(:dns_update)
|
291
|
+
application.action_install
|
292
|
+
|
293
|
+
File.unlink(application.resolver_path) if File.exists?(application.resolver_path)
|
294
|
+
File.unlink(application.launch_agent_path) if File.exists?(application.launch_agent_path)
|
295
|
+
end
|
296
|
+
|
297
|
+
it "should not create and invalid logger" do
|
298
|
+
application.stub(:resolver_path).and_return("/invalid/resolver")
|
299
|
+
application.stub(:launch_agent_path).and_return("/invalid/agent")
|
300
|
+
File.unlink(application.resolver_path) if File.exists?(application.resolver_path)
|
301
|
+
File.unlink(application.launch_agent_path) if File.exists?(application.launch_agent_path)
|
302
|
+
|
303
|
+
application.get_logger.should_receive(:error).with("Cannot create the resolver file.")
|
304
|
+
application.action_install
|
305
|
+
|
306
|
+
File.unlink(application.resolver_path) if File.exists?(application.resolver_path)
|
307
|
+
File.unlink(application.launch_agent_path) if File.exists?(application.launch_agent_path)
|
308
|
+
end
|
309
|
+
|
310
|
+
it "should not create and invalid agent" do
|
311
|
+
application.stub(:resolver_path).and_return(resolver_path)
|
312
|
+
application.stub(:launch_agent_path).and_return("/invalid/agent")
|
313
|
+
File.unlink(application.resolver_path) if File.exists?(application.resolver_path)
|
314
|
+
File.unlink(application.launch_agent_path) if File.exists?(application.launch_agent_path)
|
315
|
+
|
316
|
+
application.get_logger.should_receive(:error).with("Cannot create the launch agent.")
|
317
|
+
application.action_install
|
318
|
+
|
319
|
+
File.unlink(application.resolver_path) if File.exists?(application.resolver_path)
|
320
|
+
File.unlink(application.launch_agent_path) if File.exists?(application.launch_agent_path)
|
321
|
+
end
|
322
|
+
|
323
|
+
it "should not load an invalid agent" do
|
324
|
+
class DevDNSd::Application
|
325
|
+
def execute_command(command)
|
326
|
+
command =~ /^launchctl/ ? raise(StandardError) : system(command)
|
327
|
+
end
|
328
|
+
end
|
329
|
+
|
330
|
+
application.stub(:resolver_path).and_return(resolver_path)
|
331
|
+
application.stub(:launch_agent_path).and_return(launch_agent_path)
|
332
|
+
File.unlink(application.resolver_path) if File.exists?(application.resolver_path)
|
333
|
+
File.unlink(application.launch_agent_path) if File.exists?(application.launch_agent_path)
|
334
|
+
|
335
|
+
application.get_logger.should_receive(:error).with("Cannot load the launch agent.")
|
336
|
+
application.action_install
|
337
|
+
|
338
|
+
File.unlink(application.resolver_path) if File.exists?(application.resolver_path)
|
339
|
+
File.unlink(application.launch_agent_path) if File.exists?(application.launch_agent_path)
|
340
|
+
end
|
341
|
+
|
342
|
+
it "should raise an exception if not running on OSX" do
|
343
|
+
application.stub(:is_osx?).and_return(false)
|
344
|
+
application.get_logger.should_receive(:fatal).with("Install DevDNSd as a local resolver is only available on MacOSX.")
|
345
|
+
application.action_install.should be_false
|
346
|
+
end
|
347
|
+
end
|
348
|
+
|
349
|
+
describe "#action_uninstall" do
|
350
|
+
it "should remove the resolver" do
|
351
|
+
application.stub(:resolver_path).and_return(resolver_path)
|
352
|
+
application.stub(:launch_agent_path).and_return(launch_agent_path)
|
353
|
+
File.unlink(application.resolver_path) if File.exists?(application.resolver_path)
|
354
|
+
File.unlink(application.launch_agent_path) if File.exists?(application.launch_agent_path)
|
355
|
+
|
356
|
+
application.action_install
|
357
|
+
application.action_uninstall
|
358
|
+
File.exists?(resolver_path).should be_false
|
359
|
+
|
360
|
+
File.unlink(application.resolver_path) if File.exists?(application.resolver_path)
|
361
|
+
File.unlink(application.launch_agent_path) if File.exists?(application.launch_agent_path)
|
362
|
+
end
|
363
|
+
|
364
|
+
it "should remove the agent" do
|
365
|
+
application.stub(:resolver_path).and_return(resolver_path)
|
366
|
+
application.stub(:launch_agent_path).and_return(launch_agent_path)
|
367
|
+
File.unlink(application.resolver_path) if File.exists?(application.resolver_path)
|
368
|
+
File.unlink(application.launch_agent_path) if File.exists?(application.launch_agent_path)
|
369
|
+
|
370
|
+
DevDNSd::Logger.stub(:default_file).and_return($stdout)
|
371
|
+
application.action_install
|
372
|
+
application.action_uninstall
|
373
|
+
File.exists?(application.launch_agent_path).should be_false
|
374
|
+
|
375
|
+
File.unlink(application.resolver_path) if File.exists?(application.resolver_path)
|
376
|
+
File.unlink(application.launch_agent_path) if File.exists?(application.launch_agent_path)
|
377
|
+
end
|
378
|
+
|
379
|
+
it "should not load delete an invalid resolver" do
|
380
|
+
application.stub(:resolver_path).and_return("/invalid/resolver")
|
381
|
+
application.stub(:launch_agent_path).and_return("/invalid/agent")
|
382
|
+
|
383
|
+
application.action_install
|
384
|
+
application.get_logger.should_receive(:warn).at_least(1)
|
385
|
+
application.action_uninstall
|
386
|
+
|
387
|
+
File.unlink(application.resolver_path) if File.exists?(application.resolver_path)
|
388
|
+
File.unlink(application.launch_agent_path) if File.exists?(application.launch_agent_path)
|
389
|
+
end
|
390
|
+
|
391
|
+
it "should not delete an invalid agent" do
|
392
|
+
application.stub(:resolver_path).and_return(resolver_path)
|
393
|
+
application.stub(:launch_agent_path).and_return("/invalid/agent")
|
394
|
+
|
395
|
+
application.action_install
|
396
|
+
application.get_logger.should_receive(:warn).at_least(1)
|
397
|
+
application.action_uninstall
|
398
|
+
|
399
|
+
File.unlink(application.resolver_path) if File.exists?(application.resolver_path)
|
400
|
+
File.unlink(application.launch_agent_path) if File.exists?(application.launch_agent_path)
|
401
|
+
end
|
402
|
+
|
403
|
+
it "should not load delete invalid agent" do
|
404
|
+
application.stub(:resolver_path).and_return(resolver_path)
|
405
|
+
application.stub(:launch_agent_path).and_return("/invalid/agent")
|
406
|
+
|
407
|
+
application.action_install
|
408
|
+
application.stub(:execute_command).and_raise(StandardError)
|
409
|
+
application.get_logger.should_receive(:warn).at_least(1)
|
410
|
+
application.action_uninstall
|
411
|
+
|
412
|
+
File.unlink(application.resolver_path) if File.exists?(application.resolver_path)
|
413
|
+
File.unlink(application.launch_agent_path) if File.exists?(application.launch_agent_path)
|
414
|
+
end
|
415
|
+
|
416
|
+
it "should update the DNS cache" do
|
417
|
+
application.stub(:resolver_path).and_return(resolver_path)
|
418
|
+
application.stub(:launch_agent_path).and_return(launch_agent_path)
|
419
|
+
File.unlink(application.resolver_path) if File.exists?(application.resolver_path)
|
420
|
+
File.unlink(application.launch_agent_path) if File.exists?(application.launch_agent_path)
|
421
|
+
|
422
|
+
application.action_install
|
423
|
+
application.should_receive(:dns_update)
|
424
|
+
application.action_uninstall
|
425
|
+
|
426
|
+
File.unlink(application.resolver_path) if File.exists?(application.resolver_path)
|
427
|
+
File.unlink(application.launch_agent_path) if File.exists?(application.launch_agent_path)
|
428
|
+
end
|
429
|
+
|
430
|
+
it "should raise an exception if not running on OSX" do
|
431
|
+
application.stub(:is_osx?).and_return(false)
|
432
|
+
application.get_logger.should_receive(:fatal).with("Install DevDNSd as a local resolver is only available on MacOSX.")
|
433
|
+
application.action_uninstall.should be_false
|
434
|
+
end
|
435
|
+
end
|
436
|
+
end
|