devdnsd 3.1.2 → 4.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.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +2 -2
  3. data/.rubocop.yml +82 -0
  4. data/.travis-gemfile +4 -10
  5. data/.travis.yml +11 -5
  6. data/CHANGELOG.md +11 -0
  7. data/Gemfile +9 -8
  8. data/README.md +4 -5
  9. data/Rakefile +22 -6
  10. data/bin/devdnsd +42 -36
  11. data/config/devdnsd_config.sample +11 -11
  12. data/devdnsd.gemspec +7 -6
  13. data/doc/DevDNSd.html +6 -6
  14. data/doc/DevDNSd/{ApplicationMethods/Aliases.html → Aliases.html} +96 -100
  15. data/doc/DevDNSd/Application.html +2170 -1084
  16. data/doc/DevDNSd/Configuration.html +63 -33
  17. data/doc/DevDNSd/Errors.html +3 -3
  18. data/doc/DevDNSd/Errors/InvalidRule.html +3 -3
  19. data/doc/DevDNSd/{ApplicationMethods/System.html → OSX.html} +116 -489
  20. data/doc/DevDNSd/Rule.html +448 -749
  21. data/doc/DevDNSd/{ApplicationMethods/Server.html → Server.html} +77 -73
  22. data/doc/DevDNSd/System.html +895 -0
  23. data/doc/DevDNSd/Version.html +6 -6
  24. data/doc/_index.html +28 -27
  25. data/doc/class_list.html +6 -2
  26. data/doc/file.README.html +8 -9
  27. data/doc/file_list.html +5 -1
  28. data/doc/frames.html +1 -1
  29. data/doc/index.html +8 -9
  30. data/doc/js/full_list.js +4 -1
  31. data/doc/method_list.html +106 -84
  32. data/doc/top-level-namespace.html +3 -3
  33. data/lib/devdnsd.rb +10 -8
  34. data/lib/devdnsd/aliases.rb +171 -0
  35. data/lib/devdnsd/application.rb +93 -704
  36. data/lib/devdnsd/configuration.rb +20 -11
  37. data/lib/devdnsd/errors.rb +1 -1
  38. data/lib/devdnsd/osx.rb +217 -0
  39. data/lib/devdnsd/rule.rb +65 -94
  40. data/lib/devdnsd/server.rb +118 -0
  41. data/lib/devdnsd/system.rb +102 -0
  42. data/lib/devdnsd/version.rb +3 -3
  43. data/locales/en.yml +17 -16
  44. data/locales/it.yml +17 -16
  45. data/spec/devdnsd/application_spec.rb +188 -184
  46. data/spec/devdnsd/configuration_spec.rb +2 -2
  47. data/spec/devdnsd/rule_spec.rb +33 -34
  48. data/spec/resolver_helper.rb +10 -27
  49. data/spec/spec_helper.rb +21 -7
  50. metadata +36 -21
  51. data/doc/DevDNSd/ApplicationMethods.html +0 -125
  52. data/doc/DevDNSd/ApplicationMethods/System/ClassMethods.html +0 -538
  53. data/spec/coverage_helper.rb +0 -20
@@ -65,28 +65,37 @@ module DevDNSd
65
65
  super(file, overrides, logger)
66
66
 
67
67
  # Make sure some arguments are of correct type
68
- self.log_file = case log_file
69
- when "STDOUT" then $stdout
70
- when "STDERR" then $stderr
71
- else File.absolute_path(File.expand_path(log_file))
72
- end
68
+ self.log_file = resolve_log_file
73
69
 
74
70
  self.pid_file = File.absolute_path(File.expand_path(pid_file))
75
71
  self.port = port.to_integer
76
72
  self.log_level = log_level.to_integer
77
73
 
78
74
  # Add a default rule
79
- add_rule(/.+/, "127.0.0.1") if rules.blank?
75
+ add_rule(match: /.+/, reply: "127.0.0.1") if rules.blank?
80
76
  end
81
77
 
82
78
  # Adds a rule to the configuration.
83
79
  #
84
- # @param args [Array] The rule's arguments.
85
- # @param block [Proc] An optional block for the rule.
80
+ # @param match [String|Regexp] The pattern to match.
81
+ # @param reply [String|Symbol] The IP or hostname to reply back to the client. It can be omitted (and it will be ignored) if a block is provided.
82
+ # @param type [Symbol] The type of request to match.
83
+ # @param options [Hash] A list of options for the request.
84
+ # @param block [Proc] An optional block to compute the reply instead of using the `reply` parameter.
86
85
  # @return [Array] The current set of rule.
87
- # @see Rule.create
88
- def add_rule(*args, &block)
89
- rules << DevDNSd::Rule.create(*args, &block)
86
+ def add_rule(match: /.+/, reply: "127.0.0.1", type: :A, options: {}, &block)
87
+ rules << DevDNSd::Rule.create(match: match, reply: reply, type: type, options: options, &block)
88
+ end
89
+
90
+ private
91
+
92
+ # :nodoc:
93
+ def resolve_log_file
94
+ case log_file
95
+ when "STDOUT" then $stdout
96
+ when "STDERR" then $stderr
97
+ else File.absolute_path(File.expand_path(log_file))
98
+ end
90
99
  end
91
100
  end
92
101
  end
@@ -11,4 +11,4 @@ module DevDNSd
11
11
  class InvalidRule < ::ArgumentError
12
12
  end
13
13
  end
14
- end
14
+ end
@@ -0,0 +1,217 @@
1
+ # This file is part of the devdnsd gem. Copyright (C) 2013 and above Shogun <shogun@cowtech.it>.
2
+ # Licensed under the MIT license, which can be found at http://www.opensource.org/licenses/mit-license.php.
3
+ #
4
+
5
+ # A small DNS server to enable local .dev domain resolution.
6
+ module DevDNSd
7
+ # OSX management methods.
8
+ module OSX
9
+ # Gets the path for the resolver file.
10
+ #
11
+ # @param tld [String] The TLD to manage.
12
+ # @return [String] The path for the resolver file.
13
+ def resolver_path(tld = nil)
14
+ tld ||= @config.tld
15
+ "/etc/resolver/#{tld}"
16
+ end
17
+
18
+ # Gets the path for the launch agent file.
19
+ #
20
+ # @param name [String] The base name for the agent.
21
+ # @return [String] The path for the launch agent file.
22
+ def launch_agent_path(name = "it.cowtech.devdnsd")
23
+ ENV["HOME"] + "/Library/LaunchAgents/#{name}.plist"
24
+ end
25
+
26
+ # Executes a shell command.
27
+ #
28
+ # @param command [String] The command to execute.
29
+ # @return [Boolean] `true` if command succeeded, `false` otherwise.
30
+ def execute_command(command)
31
+ system("#{command} 2>&1 > /dev/null")
32
+ end
33
+
34
+ # Updates DNS cache.
35
+ #
36
+ # @return [Boolean] `true` if command succeeded, `false` otherwise.
37
+ def dns_update
38
+ @logger.info(i18n.dns_update)
39
+
40
+ script = Tempfile.new("devdnsd-dns-cache-script")
41
+ script.write("dscacheutil -flushcache 2>&1 > /dev/null\n")
42
+ script.write("killall -9 mDNSResponder 2>&1 > /dev/null\n")
43
+ script.write("killall -9 mDNSResponderHelper 2>&1 > /dev/null\n")
44
+ script.close
45
+
46
+ Kernel.system("/usr/bin/osascript -e 'do shell script \"sh #{script.path}\" with administrator privileges' 2>&1 > /dev/null")
47
+ script.unlink
48
+ end
49
+
50
+ # Checks if we are running on MacOS X.
51
+ #
52
+ # System services are only available on that platform.
53
+ #
54
+ # @return [Boolean] `true` if the current platform is MacOS X, `false` otherwise.
55
+ def osx?
56
+ ::RbConfig::CONFIG['host_os'] =~ /^darwin/
57
+ end
58
+ alias_method :is_osx?, :osx?
59
+
60
+ # Adds aliases to an interface.
61
+ #
62
+ # @param options [Hash] The options provided by the user.
63
+ # @return [Boolean] `true` if action succeeded, `false` otherwise.
64
+ def action_add(options)
65
+ manage_aliases(:add, i18n.add_empty, options)
66
+ end
67
+
68
+ # Removes aliases from an interface.
69
+ #
70
+ # @param options [Hash] The options provided by the user.
71
+ # @return [Boolean] `true` if action succeeded, `false` otherwise.
72
+ def action_remove(options)
73
+ manage_aliases(:remove, i18n.remove_empty, options)
74
+ end
75
+
76
+ # Installs the application into the autolaunch.
77
+ #
78
+ # @return [Boolean] `true` if action succeeded, `false` otherwise.
79
+ def action_install
80
+ manage_installation(launch_agent_path, resolver_path, :create_resolver, :create_agent, :load_agent)
81
+ end
82
+
83
+ # Uninstalls the application from the autolaunch.
84
+ #
85
+ # @return [Boolean] `true` if action succeeded, `false` otherwise.
86
+ def action_uninstall
87
+ manage_installation(launch_agent_path, resolver_path, :delete_resolver, :unload_agent, :delete_agent)
88
+ end
89
+
90
+ private
91
+
92
+ # :nodoc:
93
+ def log_status(pid, status)
94
+ logger.info(status == :running ? replace_markers(i18n.status_running(pid)) : replace_markers(i18n.send("status_#{status}")))
95
+ end
96
+
97
+ # :nodoc:
98
+ def manage_installation(launch_agent, resolver_path, first_operation, second_operation, third_operation)
99
+ rv = check_agent_available
100
+
101
+ logger.warn(replace_markers(i18n.admin_privileges_warning))
102
+ rv = send(first_operation, launch_agent, resolver_path) if rv
103
+ rv = send(second_operation, launch_agent, resolver_path) if rv
104
+ rv = send(third_operation, launch_agent, resolver_path) if rv
105
+ dns_update
106
+ rv
107
+ end
108
+
109
+ # :nodoc:
110
+ def delete_file(file, before_message, error_message)
111
+ logger.info(i18n.send(before_message, file))
112
+ ::File.delete(file)
113
+ true
114
+ rescue
115
+ logger.warn(i18n.send(error_message))
116
+ false
117
+ end
118
+
119
+ # :nodoc:
120
+ def check_agent_available
121
+ rv = true
122
+ unless osx?
123
+ logger.fatal(i18n.no_agent)
124
+ rv = false
125
+ end
126
+
127
+ rv
128
+ end
129
+
130
+ # :nodoc:
131
+ def create_resolver(_, resolver_path)
132
+ logger.info(replace_markers(i18n.resolver_creating(resolver_path)))
133
+
134
+ script_file = create_resolver_script(resolver_path)
135
+ Kernel.system("/usr/bin/osascript -e 'do shell script \"sh #{script_file.path}\" with administrator privileges' 2>&1 > /dev/null")
136
+ script_file.unlink
137
+ true
138
+ rescue
139
+ logger.error(i18n.resolver_creating_error)
140
+ false
141
+ end
142
+
143
+ # :nodoc:
144
+ def create_resolver_script(resolver_path)
145
+ script = "mkdir -p '#{File.dirname(resolver_path)}'\nrm -rf '#{resolver_path}'\necho 'nameserver 127.0.0.1\\nport #{@config.port}' >> '#{resolver_path}'"
146
+ f = Tempfile.new("devdnsd-install-script")
147
+ f.write(script)
148
+ f.close
149
+ f
150
+ end
151
+
152
+ # :nodoc:
153
+ def delete_resolver(_, resolver_path)
154
+ logger.info(i18n.resolver_deleting(resolver_path))
155
+ Kernel.system("/usr/bin/osascript -e 'do shell script \"rm #{resolver_path}\" with administrator privileges' 2>&1 > /dev/null")
156
+ true
157
+ rescue
158
+ logger.warn(i18n.resolver_deleting_error)
159
+ false
160
+ end
161
+
162
+ # :nodoc:
163
+ def create_agent(launch_agent, _)
164
+ logger.info(replace_markers(i18n.agent_creating(launch_agent)))
165
+ program, args = prepare_agent
166
+
167
+ ::File.open(launch_agent, "w") do |f|
168
+ f.write({"KeepAlive" => true, "Label" => "it.cowtech.devdnsd", "Program" => program, "ProgramArguments" => args, "RunAtLoad" => true}.to_plist)
169
+ f.flush
170
+ end
171
+
172
+ true
173
+ rescue
174
+ logger.error(i18n.agent_creating_error)
175
+ false
176
+ end
177
+
178
+ # :nodoc:
179
+ def prepare_agent
180
+ [
181
+ (::Pathname.new(Dir.pwd) + $PROGRAM_NAME).to_s,
182
+ (ARGV ? ARGV[0, ARGV.length - 1] : [])
183
+ ]
184
+ end
185
+
186
+ # :nodoc:
187
+ def delete_agent(launch_agent, _)
188
+ delete_file(launch_agent, :agent_deleting, :agent_deleting_error)
189
+ end
190
+
191
+ # :nodoc:
192
+ def load_agent(launch_agent, _)
193
+ toggle_agent(launch_agent, "load", :agent_loading, :agent_loading_error, :error)
194
+ end
195
+
196
+ # :nodoc:
197
+ def unload_agent(launch_agent, _)
198
+ toggle_agent(launch_agent, "unload", :agent_unloading, :agent_unloading_error, :warn)
199
+ end
200
+
201
+ # :nodoc:
202
+ def toggle_agent(launch_agent, operation, info_message, error_message, error_level)
203
+ logger.info(i18n.send(info_message, launch_agent))
204
+ raise RuntimeError unless File.exist?(launch_agent)
205
+ execute_command("launchctl #{operation} -w \"#{launch_agent}\" > /dev/null 2>&1")
206
+ true
207
+ rescue
208
+ logger.send(error_level, i18n.send(error_message))
209
+ false
210
+ end
211
+
212
+ # :nodoc:
213
+ def replace_markers(message)
214
+ @command.application.console.replace_markers(message)
215
+ end
216
+ end
217
+ end
@@ -24,19 +24,54 @@ module DevDNSd
24
24
  attr_accessor :options
25
25
  attr_accessor :block
26
26
 
27
- include Lazier::I18n
27
+ # Class methods
28
+ class << self
29
+ # Creates a new rule.
30
+ #
31
+ # @param match [String|Regexp] The pattern to match.
32
+ # @param reply [String|Symbol] The IP or hostname to reply back to the client. It can be omitted (and it will be ignored) if a block is provided.
33
+ # @param type [Symbol] The type of request to match.
34
+ # @param options [Hash] A list of options for the request.
35
+ # @param block [Proc] An optional block to compute the reply instead of using the `reply` parameter.
36
+ # @return [Rule] The new rule.
37
+ def create(match: /.+/, reply: "127.0.0.1", type: :A, options: {}, &block)
38
+ new(match: match, reply: reply, type: type, options: options, &block)
39
+ end
40
+
41
+ # Converts a class to the correspondent symbol.
42
+ #
43
+ # @param klass [Class] The class to convert.
44
+ # @return [Symbol] The symbol representation of the class.
45
+ def resource_class_to_symbol(klass)
46
+ klass.to_s.gsub(/(.+::)?(.+)/, "\\2").to_sym
47
+ end
48
+
49
+ # Converts a symbol to the correspondent DNS resource class.
50
+ #
51
+ # @param symbol [Symbol] The symbol to convert.
52
+ # @param locale [Symbol] The locale to use for the messages.
53
+ # @return [Symbol] The class associated to the symbol.
54
+ def symbol_to_resource_class(symbol, locale = nil)
55
+ symbol = symbol.to_s.upcase
56
+
57
+ begin
58
+ "Resolv::DNS::Resource::IN::#{symbol}".constantize
59
+ rescue ::NameError
60
+ i18n = Bovem::I18n.new(locale, root: "devdnsd", path: ::Pathname.new(::File.dirname(__FILE__)).to_s + "/../../locales/")
61
+ raise(DevDNSd::Errors::InvalidRule, i18n.rule_invalid_resource(symbol))
62
+ end
63
+ end
64
+ end
28
65
 
29
66
  # Creates a new rule.
30
67
  #
31
68
  # @param match [String|Regexp] The pattern to match.
32
- # @param reply [String] The IP or hostname to reply back to the client.
69
+ # @param reply [String|Symbol] The IP or hostname to reply back to the client. It can be omitted (and it will be ignored) if a block is provided.
33
70
  # @param type [Symbol] The type of request to match.
34
71
  # @param options [Hash] A list of options for the request.
35
72
  # @param block [Proc] An optional block to compute the reply instead of using the `reply` parameter.
36
73
  # @see .create
37
- def initialize(match = /.+/, reply = "127.0.0.1", type = :A, options = {}, &block)
38
- i18n_setup(:devdnsd, ::File.absolute_path(::Pathname.new(::File.dirname(__FILE__)).to_s + "/../../locales/"))
39
- self.i18n = options[:locale]
74
+ def initialize(match: /.+/, reply: "127.0.0.1", type: :A, options: {}, &block)
40
75
  setup(match, reply, type, options, block)
41
76
  validate_rule
42
77
  end
@@ -45,122 +80,58 @@ module DevDNSd
45
80
  #
46
81
  # @return [Array|Class] The class(es) for the current rule.
47
82
  def resource_class
48
- classes = @type.ensure_array(nil, true, true, true) {|cls| self.class.symbol_to_resource_class(cls, options[:locale]) }
83
+ classes = @type.ensure_array(no_duplicates: true, compact: true, flatten: true) { |cls| self.class.symbol_to_resource_class(cls, options[:locale]) }
49
84
  classes.length == 1 ? classes.first : classes
50
85
  end
51
86
 
52
87
  # Checks if the rule is a regexp.
53
88
  #
54
89
  # @return [Boolean] `true` if the rule is a Regexp, `false` otherwise.
55
- def is_regexp?
90
+ def regexp?
56
91
  @match.is_a?(::Regexp)
57
92
  end
93
+ alias_method :is_regexp?, :regexp?
58
94
 
59
95
  # Checks if the rule is a regexp.
60
96
  #
61
97
  # @return [Boolean] `true` if the rule has a block, `false` otherwise.
62
- def has_block?
98
+ def block?
63
99
  @block.present?
64
100
  end
101
+ alias_method :has_block?, :block?
65
102
 
66
103
  # Matches a hostname to the rule.
67
104
  #
68
105
  # @param hostname [String] The hostname to match.
69
106
  # @return [MatchData|Boolean|Nil] Return `true` or MatchData (if the pattern is a regexp) if the rule matches, `false` or `nil` otherwise.
70
107
  def match_host(hostname)
71
- is_regexp? ? @match.match(hostname) : (@match == hostname)
72
- end
73
-
74
- # Creates a new rule.
75
- #
76
- # @param match [String|Regexp] The pattern to match.
77
- # @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).
78
- # @param type [Symbol] The type of request to match. This is ignored if a block is provided.
79
- # @param options [Hash] A list of options for the request.
80
- # @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.
81
- # @return [Rule] The new rule.
82
- def self.create(match, reply_or_type = nil, type = nil, options = {}, &block)
83
- validate_options(reply_or_type, options, block, Lazier::Localizer.new(:devdnsd, ::File.absolute_path(::Pathname.new(::File.dirname(__FILE__)).to_s + "/../../locales/"), options.is_a?(Hash) ? options[:locale] : nil))
84
- setup(new(match), reply_or_type, type, options, block)
85
- end
86
-
87
- # Converts a class to the correspondent symbol.
88
- #
89
- # @param klass [Class] The class to convert.
90
- # @return [Symbol] The symbol representation of the class.
91
- def self.resource_class_to_symbol(klass)
92
- klass.to_s.gsub(/(.+::)?(.+)/, "\\2").to_sym
108
+ regexp? ? @match.match(hostname) : (@match == hostname)
93
109
  end
94
110
 
95
- # Converts a symbol to the correspondent DNS resource class.
96
- #
97
- # @param symbol [Symbol] The symbol to convert.
98
- # @param locale [Symbol] The locale to use for the messages.
99
- # @return [Symbol] The class associated to the symbol.
100
- def self.symbol_to_resource_class(symbol, locale = nil)
101
- symbol = symbol.to_s.upcase
111
+ private
102
112
 
103
- begin
104
- "Resolv::DNS::Resource::IN::#{symbol}".constantize
105
- rescue ::NameError
106
- raise(DevDNSd::Errors::InvalidRule.new(Lazier::Localizer.new(:devdnsd, ::File.absolute_path(::Pathname.new(::File.dirname(__FILE__)).to_s + "/../../locales/"), locale).i18n.invalid_class(symbol)))
107
- end
108
- end
113
+ # :nodoc:
114
+ def setup(match, reply, type, options, block)
115
+ @match = match
109
116
 
110
- private
111
- # Setups a new rule.
112
- #
113
- # @param match [String|Regexp] The pattern to match.
114
- # @param reply [String] The IP or hostname to reply back to the client.
115
- # @param type [Symbol] The type of request to match.
116
- # @param options [Hash] A list of options for the request.
117
- # @param block [Proc] An optional block to compute the reply instead of using the `reply` parameter.
118
- def setup(match, reply, type, options, block)
119
- @match = match
117
+ if block.present? # reply acts like a type, type is ignored
118
+ @type = type || :A
119
+ @reply = nil
120
+ else # reply acts like a reply
121
+ @reply = reply || "127.0.0.1"
120
122
  @type = type || :A
121
- @reply = block.blank? ? (reply || "127.0.0.1") : nil
122
- @options = options
123
- @block = block
124
- end
125
-
126
- # Validates a newly created rule.
127
- def validate_rule
128
- raise(DevDNSd::Errors::InvalidRule.new(i18n.rule_invalid_call)) if @reply.blank? && @block.nil?
129
- raise(DevDNSd::Errors::InvalidRule.new(i18n.rule_invalid_options)) if !@options.is_a?(::Hash)
130
123
  end
131
124
 
132
- # Setups a new rule.
133
- #
134
- # @param rv [Rule] The rule that is been created.
135
- # @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).
136
- # @param type [Symbol] The type of request to match. This is ignored if a block is provided.
137
- # @param options [Hash] A list of options for the request.
138
- # @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.
139
- # @return [Rule] The new rule.
140
- def self.setup(rv, reply_or_type, type, options = {}, block)
141
- rv.options = options
142
- rv.block = block
143
-
144
- if block.present? then # reply_or_type acts like a type, type is ignored
145
- rv.type = reply_or_type || :A
146
- rv.reply = nil
147
- else # reply_or_type acts like a reply
148
- rv.reply = reply_or_type || "127.0.0.1"
149
- rv.type = type || :A
150
- end
151
-
152
- rv
153
- end
125
+ @options = options
126
+ @block = block
127
+ locale = options.is_a?(Hash) ? options[:locale] : :en
128
+ @i18n = Bovem::I18n.new(locale, root: "devdnsd", path: ::Pathname.new(::File.dirname(__FILE__)).to_s + "/../../locales/")
129
+ end
154
130
 
155
- # Validate options for a new rule creation.
156
- #
157
- # @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).
158
- # @param options [Hash] A list of options for the request.
159
- # @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.
160
- # @param localizer [Localizer] A localizer object.
161
- def self.validate_options(reply_or_type, options, block, localizer)
162
- raise(DevDNSd::Errors::InvalidRule.new(localizer.i18n.rule_invalid_call)) if reply_or_type.blank? && block.nil?
163
- raise(DevDNSd::Errors::InvalidRule.new(localizer.i18n.rule_invalid_options)) if !options.is_a?(::Hash)
164
- end
131
+ # Validates a newly created rule.
132
+ def validate_rule
133
+ raise(DevDNSd::Errors::InvalidRule, @i18n.rule_invalid_call) if @reply.blank? && @block.nil?
134
+ raise(DevDNSd::Errors::InvalidRule, @i18n.rule_invalid_options) unless @options.is_a?(::Hash)
135
+ end
165
136
  end
166
137
  end