dnsomatic 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,53 @@
1
+ #!/usr/bin/ruby
2
+
3
+ begin
4
+ require 'dnsomatic'
5
+ rescue LoadError => e
6
+ # a small hack in the case where we aren't a gem
7
+ $: << File.join(File.dirname($0), '..', 'lib')
8
+ retry
9
+ end
10
+
11
+ begin
12
+ $opts = DNSOMatic::Opts.instance
13
+ $opts.parse(ARGV)
14
+
15
+ c = DNSOMatic::Config.new($opts.cf)
16
+
17
+ if $opts.showcf
18
+ $stdout.puts "This is your configuration after merging in the defaults."
19
+ $stdout.puts "It has been pruned to only display the stanza named: #{$opts.name}" if $opts.name
20
+ $stdout.puts c.merged_config($opts.name)
21
+ exit
22
+ end
23
+
24
+ if $opts.stanza and $opts.verbose
25
+ $stdout.puts "Will only update #{$opts.name}."
26
+ end
27
+
28
+ updaters = c.updaters($opts.name)
29
+
30
+ updaters.each_pair do |name, obj|
31
+ $stdout.puts "Working with host update definition: #{name}" if $opts.verbose
32
+ if $opts.print
33
+ puts obj
34
+ else
35
+ obj.send( $opts.force ? 'update!' : 'update')
36
+ end
37
+ end
38
+ rescue DNSOMatic::Error => e
39
+ $stderr.puts e
40
+ if $opts.debug
41
+ $stderr.puts "Backtrace:"
42
+ $stderr.puts e.backtrace
43
+ end
44
+ rescue => e
45
+ $stderr.puts "Rescued an unhandled exception of type: #{e.class}"
46
+ $stderr.puts "The exception contains the following message:"
47
+ $stderr.puts e.message
48
+ if $opts.debug
49
+ $stderr.puts "Backtrace:"
50
+ $stderr.puts e.backtrace
51
+ end
52
+ exit 1
53
+ end
@@ -0,0 +1,162 @@
1
+ require 'yaml'
2
+ require 'open-uri'
3
+
4
+ # :title: dnsomatic - a DNS-o-Matic update client
5
+ #
6
+ # = dnsomatic - a DNS-o-Matic update client
7
+ #
8
+ # Author:: Ben Walton (mailto: bdwalton@gmail.com)
9
+ # Copyright:: Copyright (c) 2008 Ben Walton
10
+ # License:: GNU General Public License (GPL) version 3
11
+ #
12
+ # = Summary
13
+ #
14
+ # By default, dnsomatic tries to avoid polling or updating its address too
15
+ # often. It will not check a remote IP lookup url more often than every half
16
+ # hour. It will avoid updating DNS-o-Matic for up to 15 days if the IP
17
+ # detected hasn't changed.
18
+ #
19
+ # = Configuration
20
+ #
21
+ # Configuration is specified by a small YAML file with a default location
22
+ # of $HOME/.dnsomatic.cf
23
+ #
24
+ # A basic configuration can be as simple as:
25
+ # defaults:
26
+ # username: YOUR_DNSOMATIC_USERNAME
27
+ # password: YOUR_DNSOMATIC_PASSWORD
28
+ #
29
+ # The dnsomatic client may update all of your registered DNS-o-Matic names
30
+ # in one shot or individual names one at a time. The defaults: stanza in the
31
+ # configuration provides default values for any hostname update stanzas to
32
+ # follow, although no extra stanzas are required.
33
+ #
34
+ # To provide alternate stanzas, add items in your config like:
35
+ # myhost:
36
+ # hostname: myhost.dyndns.foo
37
+ # mx: mx.example.net
38
+ #
39
+ # When that stanza is used in conjunction with the above defaults stanza, you
40
+ # have a fully operation host update definition (myhost: would inherit the
41
+ # username and password from defaults:)
42
+ #
43
+ # In some cases it may be desirable to fetch the Web visible IP for a system
44
+ # from a different URL than the dnsomatic option (http://myip.dnsomatic.com).
45
+ # To allow for this, you may add a webipfetchurl: paramter to a confiruation
46
+ # stanza (or defaults:). Consider:
47
+ # otherhost:
48
+ # hostname: otherhost.dyndns.foo
49
+ # webipfetchurl: http://ipfetch.example.net/
50
+ #
51
+ # If all three of the above stanzas were in the same configuration file, you
52
+ # would send two update requests (conditional upon changed IP addresses and/or
53
+ # expiration times), with the second polling for its IP at the
54
+ # ipfetch.example.net url instead of the default myip.dnsomatic.com service.
55
+ #
56
+ # When there are update stanzas named specifically (in addition to defaults:),
57
+ # defaults is used only to provide default values for the other stanzas. For
58
+ # most users wanting to define more than one updater stanza, you would provide
59
+ # the username and password via the defaults: stanza and then a hostname value
60
+ # for the individual stanza definitions. Once the defaults are merged into the
61
+ # specific stanzas, they are ignored. To see how the merging works, you may
62
+ # use the 'display' option (see options with --help), which will dump to stdout
63
+ # a YAML serialized view of the merged configuration.
64
+ #
65
+ # This allows you to obtain an IP from multiple (or just alternate services).
66
+ # Although this might sound less than useful, it would be handy if you wanted
67
+ # different IP's associated with machines that could route to the internet via
68
+ # two different public IP's, or if you wanted to poll an internal service and
69
+ # update a public name with an internal IP.
70
+ #
71
+ # A complete list of options that may be specified in the defaults: or a named
72
+ # update definition are:
73
+ # * username - your dnsomatic username
74
+ # * password - your dnsomatic password
75
+ # * mx - a hostname that will handle mail delivery for this host. it must
76
+ # resolve to an IP or DNS-o-Matic will ignore it.
77
+ # * backmx - a lower priority mx record. sames rules as mx. you may also list
78
+ # these as NOCHG, which tells DNS-o-Matic to leave them as is.
79
+ # * hostname - the hostname to update. the defaults specify this as
80
+ # all.dnsomatic.com, which tells dnsomatic to update all listed
81
+ # records with the same values.
82
+ # * wildcard - indicates whether foo.hostname and bar.hostname and baz.hostname should also resolve to the same IP as hostname.
83
+ # - ON = enable
84
+ # - NOCHG = leave it as is
85
+ # - _other_ = disable
86
+ # * offline - sets the hostname of offline mode, which may do some redirection
87
+ # things depending on the service being updated.
88
+ # - YES = enable
89
+ # - NOCHG = leave it as is
90
+ # - _other_ = disable
91
+ #
92
+ # = Usage
93
+ #
94
+ # Usage: dnsomatic [options]
95
+ # -m, --minimum SEC The minimum time between updates (def: 30m)
96
+ # -M, --maximum SEC The maximum time between updates (def: 15d)
97
+ # -a, --alert Emit an alert if the IP is updated
98
+ # -n, --name NAME Only update host stanza NAME
99
+ # -d, --display-config Display the configuration and exit
100
+ # -c, --config FILE Use an alternate config file
101
+ # -f, --force Force an update, even if IP is unchanged
102
+ # -p, --print Output the update URLs. No action taken
103
+ # -v, --verbose Display runtime messages
104
+ # -V, --version Display version and exit
105
+ # -x, --debug Output additional info in error situations
106
+ # -h, --help Display this help text
107
+
108
+ module DNSOMatic
109
+ VERSION = '0.1.1'
110
+ USERAGENT = "Ruby_DNS-o-Matic/#{VERSION}"
111
+
112
+ # We provide our easily distinguishable exception class so that we can easily
113
+ # differntiate our errors from others.
114
+ class Error < Exception; end
115
+
116
+ def self.http_fetch (url)
117
+ uri = URI.parse(url)
118
+
119
+ begin
120
+ res = if uri.user and uri.password
121
+ open(url, 'User-Agent' => USERAGENT,
122
+ :http_basic_authentication => [uri.user, uri.password])
123
+ else
124
+ open(url, 'User-Agent' => USERAGENT)
125
+ end
126
+ res.read
127
+ rescue OpenURI::HTTPError, SocketError => e
128
+ msg = "Error communicating with #{uri.host}\n"
129
+ msg += "Message was: #{e.message}\n"
130
+ msg += "Full URL being requested: #{uri}"
131
+ raise(DNSOMatic::Error, msg)
132
+ end
133
+ end
134
+
135
+ def self.yaml_read(file)
136
+ begin
137
+ yaml = YAML::load(File.open(file))
138
+ rescue Exception => e
139
+ msg = "An exception (#{e.class}) occurred while reading a yaml file: #{file}\n"
140
+ msg += "The error message is: #{e.message}"
141
+ raise(DNSOMatic::Error, msg)
142
+ end
143
+ end
144
+
145
+ def self.yaml_write(file, data)
146
+ begin
147
+ File.open(file, 'w') do |f|
148
+ f.puts data.to_yaml
149
+ end
150
+ rescue Exception => e
151
+ msg = "An exception (#{e.class}) occurred while writing a yaml file: #{file}\n"
152
+ msg += "The error message is: #{e.message}"
153
+ raise(DNSOMatic::Error, msg)
154
+ end
155
+ end
156
+ end
157
+
158
+ require 'dnsomatic/opts'
159
+ require 'dnsomatic/config'
160
+ require 'dnsomatic/updater'
161
+ require 'dnsomatic/iplookup'
162
+ require 'dnsomatic/logger'
@@ -0,0 +1,142 @@
1
+ require 'net/https'
2
+
3
+ module DNSOMatic
4
+ # A class to handle 'parsing' the configuration files and setting defaults
5
+ # as required. Config files are actually YAML files, so the parsing is
6
+ # offloaded for the most part.
7
+ class Config
8
+ #in most cases, a user can simply set username and password in a defaults:
9
+ #stanza and fire the client.
10
+ @@defaults = { 'hostname' => 'all.dnsomatic.com',
11
+ 'wildcard' => 'NOCHG',
12
+ 'mx' => 'NOCHG',
13
+ 'backmx' => 'NOCHG',
14
+ 'offline' => 'NOCHG',
15
+ 'webipfetchurl' => 'http://myip.dnsomatic.com/' }
16
+
17
+ # Create a new instance with the option of specifying an alternate config
18
+ # file to read. The default config file is either $HOME/.dnsomatic.cf (on
19
+ # unix or if Windows specifies a $HOME environment variable) or
20
+ # %APPDATA%/.dnsomatic.cf for most Windows environments.
21
+ def initialize(conffile = nil)
22
+ stdcf = File.join(ENV['HOME'] || ENV['APPDATA'], '.dnsomatic.cf')
23
+ if conffile
24
+ if File.exists?(conffile)
25
+ @cf = conffile
26
+ else
27
+ raise(DNSOMatic::ConfErr, "Invalid config file: #{conffile}")
28
+ end
29
+ elsif File.exists?(stdcf)
30
+ @cf = stdcf
31
+ else
32
+ #in this case, the values from the defaults: stanza in the default conf
33
+ #will provide all values required.
34
+ @cf = nil
35
+ end
36
+
37
+ @updaters = nil
38
+ @config = {}
39
+
40
+ # the user config must supply values for these, either in a specific
41
+ # host updater stanza or by overriding the global default in defaults:
42
+ @req_conf = %w(username password)
43
+
44
+ msg = @cf.nil? ? "Couldn't find a user config. Using defaults only." : \
45
+ "Using config file #{@cf}"
46
+ Logger::log(msg)
47
+
48
+ load()
49
+ end
50
+
51
+ # Provide a view of the configuration file after merging defaults. This
52
+ # is returned as a YAML string, suitable for redirecting right into a
53
+ # config file. Optionally, the return value can be 'pruned' to show only
54
+ # one host update stanza by passing in the desired name as a string.
55
+ def merged_config(prune_to = nil)
56
+ prune_to.nil? ? @config.to_yaml : one_key(@config, prune_to).to_yaml
57
+ end
58
+
59
+ # Return a list of Updater objects. Each host update stanza is turned into
60
+ # an individual object. This may be pruned to a single Updater by passing
61
+ # in the name of the stanza title.
62
+ def updaters(prune_to = nil)
63
+ #don't create updater objects until they're actually requested.
64
+ #(saves a little overhead if just displaying the config)
65
+
66
+ if @updaters.nil?
67
+ @updaters = {}
68
+ @config.each_key do |token|
69
+ @updaters[token] = Updater.new(@config[token])
70
+ end
71
+ end
72
+
73
+ prune_to.nil? ? @updaters : one_key(@updaters, prune_to)
74
+ end
75
+
76
+ private
77
+
78
+ def one_key(hsh, key)
79
+ if ! hsh.has_key?(key)
80
+ msg = "Invalid host stanza filter ('#{key}').\n"
81
+ msg += "You config doesn't define anything with that name."
82
+ raise(DNSOMatic::ConfErr, msg)
83
+ else
84
+ { key => hsh[key] }
85
+ end
86
+ end
87
+
88
+ def load
89
+ return @@defaults if @cf.nil?
90
+
91
+ conf = DNSOMatic::yaml_read(@cf)
92
+ raise DNSOMatic::ConfErr, "Invalid configuration format in #{@cf}" unless conf.kind_of?(Hash)
93
+
94
+ if conf.has_key?('defaults')
95
+ #allow the user to override our built-in defaults
96
+ @@defaults.merge!(conf['defaults'])
97
+ #if they've provided only the defaults stanza, we'll use it to perform
98
+ #the update, otherwise remove it as it has been folded into @@defaults
99
+ conf.delete('defaults') if conf.keys.size > 1
100
+ end
101
+
102
+ conf.each_key do |token|
103
+ stanza = @@defaults.merge(conf[token])
104
+ @req_conf.each do |required|
105
+ #still test for existence in case the defaults get munged.
106
+ if !stanza.has_key?(required) or stanza[required].nil?
107
+ msg = "Invalid configuration for Host Updater named '#{token}'\n"
108
+ msg += "Please define the field: #{required}."
109
+ raise DNSOMatic::ConfErr, msg
110
+ end
111
+ end
112
+
113
+ #just in case
114
+ stanza.each_pair do |k,v|
115
+ stanza[k] = fmt(v)
116
+ end
117
+
118
+ #save our merged version in case we're just dump our config to stdout
119
+ @config[token] = stanza
120
+ end
121
+ end
122
+
123
+ def fmt(val)
124
+ #because YAML interprets a raw YES or NO as a boolean true/false and we
125
+ #don't want to burden user with prefixing the value with !str, we'll
126
+ #attempt to deduce what they meant here...
127
+ if [TrueClass, FalseClass].include?(val.class)
128
+ val ? 'ON' : 'OFF'
129
+ elsif val.kind_of?(NilClass)
130
+ 'NOCHG'
131
+ elsif val.kind_of?(String)
132
+ case val.downcase
133
+ when 'no': 'OFF'
134
+ when 'yes': 'ON'
135
+ else val.gsub(/\s+/, '')
136
+ end
137
+ else
138
+ val.to_s.gsub(/\s+/, '')
139
+ end
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,142 @@
1
+ require 'singleton'
2
+ require 'date'
3
+
4
+ module DNSOMatic
5
+ # This class handles actually fetching the remotely (not necessarily internet)
6
+ # visible IP for a provided URL. It manages both a minimum delay between
7
+ # checks (default 30m) and a maximum time before it reports a CHANGED status
8
+ # regardless of the result from the remote ip lookup url.
9
+ class IPStatus
10
+ CHANGED = true
11
+ UNCHANGED = false
12
+
13
+ attr_reader :ip
14
+
15
+ # A URL must be provided that returns the IP of the system that requests
16
+ # the URL. A commonly used example is http://www.whatismyip.org
17
+ def initialize(url)
18
+ @url = url
19
+ @status = CHANGED #all new lookups are changed.
20
+ @ip = getip()
21
+ @last_update = Time.now
22
+ Logger::log("Fetched new IP #{@ip} from #{@url}.")
23
+ end
24
+
25
+ # Tell a client whether or not we have a different IP than the last time
26
+ # we were asked. This may be faked in the case where we've reached the
27
+ # the maximum time we allow before forcing an update.
28
+ def changed?
29
+ @status
30
+ end
31
+
32
+ # Calling this method requests that we retrieve our IP from the remote URL
33
+ # and potentially alter our status as returned by changed?.
34
+ def update
35
+ if min_elapsed?
36
+ Logger::log("Returned cached IP #{@ip} from #{@url}.")
37
+ @status = UNCHANGED
38
+ else
39
+ ip = getip()
40
+ @last_update = @status ? Time.now : @last_update
41
+
42
+ if !@ip.eql?(ip)
43
+ Logger::log("Detected IP change (#{@ip} -> #{ip}) from #{@url}.")
44
+ else
45
+ Logger::log("No IP change detected from #{@url}.")
46
+ end
47
+
48
+ @status = (max_elapsed? or !@ip.eql?(ip)) ? CHANGED : UNCHANGED
49
+ @ip = ip
50
+ end
51
+ end
52
+
53
+ private
54
+ def min_elapsed?
55
+ if Time.now - @last_update <= $opts.minimum
56
+ Logger::log("Minimum lookup interval not expired.")
57
+ true
58
+ else
59
+ false
60
+ end
61
+ end
62
+
63
+ def max_elapsed?
64
+ if Time.now - @last_update >= $opts.maximum
65
+ Logger::log("Maximum interval between updates has elapsed. Update will be forced.")
66
+ true
67
+ else
68
+ false
69
+ end
70
+ end
71
+
72
+ def getip
73
+ ip = DNSOMatic::http_fetch(@url)
74
+ if !ip.match(/(\d{1,3}\.){3}\d{1,3}/)
75
+ msg = "Strange return value from IP Lookup service: #{@url}\n"
76
+ msg += "Body of HTTP response was:\n"
77
+ msg += ip
78
+ raise(DNSOMatic::Error, msg)
79
+ else
80
+ ip
81
+ end
82
+ end
83
+ end
84
+
85
+ # Any callers wishing to use IPStatus objects should request them via an
86
+ # instance of IPLookup. IPLookup is a singleton object that will cache
87
+ # IPStatus objects across sessions by persisting them to a YAML file.
88
+ class IPLookup
89
+ include Singleton
90
+
91
+ # Allow a caller to toggle the cache on and off.
92
+ attr_accessor :persist
93
+
94
+ # Because we're a singleton, we don't take any initialization arguments.
95
+ def initialize
96
+ fn = 'dnsomatic-' + Process.uid.to_s + '.cache'
97
+ @cache_file = File.join(ENV['TEMP'] || '/tmp', fn)
98
+
99
+ @cache = Hash.new
100
+ @persist = true
101
+ end
102
+
103
+ # This allows callers to alter which file we will persist our cache of
104
+ # IPStatus objects to.
105
+ def setcachefile(file)
106
+ if !File.writable?(File.dirname(file))
107
+ raise(DNSOMatic::Error, "Unwritable cache file directory.")
108
+ elsif File.exists?(file) and !File.writable(file)
109
+ raise(DNSOMatic::Error, "Unwritable cache file")
110
+ end
111
+ @cache_file = file
112
+ end
113
+
114
+ # This is the method a caller would use to request and IPStatus object.
115
+ # The url is passed to new() when the object is created or returned from
116
+ # the cache.
117
+ def ip_from_url(url)
118
+ load()
119
+ #implement a simple cache to prevent making multiple http requests
120
+ #to the same remote agent (in the case where a user defines multiple
121
+ #updater stanzas that use the same ip fetch url).
122
+ #because an access for a key that doesn't exist returns and inserts
123
+ #a new IPStatus object, we don't differntiate between seen and unseen
124
+ #here.
125
+ (@cache[url] ||= IPStatus.new(url)).update
126
+
127
+ save() #ensure that we get spooled to disk.
128
+ @cache[url]
129
+ end
130
+
131
+ private
132
+ def load
133
+ if File.exists?(@cache_file) and @persist
134
+ @cache = DNSOMatic::yaml_read(@cache_file)
135
+ end
136
+ end
137
+
138
+ def save
139
+ DNSOMatic::yaml_write(@cache_file, @cache) if @persist
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,21 @@
1
+ module DNSOMatic
2
+ # A simple class to provide consistent logging throughout the various other
3
+ # classes. All methods are class methods, so no instance is required.
4
+ class Logger
5
+ # Output a message to stdout if the user specified the verbose command
6
+ # line option.
7
+ def self.log(msg)
8
+ $stdout.puts msg if $opts.verbose
9
+ end
10
+
11
+ # Output a message to stderr regardless of verbose command line option.
12
+ def self.warn(msg)
13
+ $stderr.puts msg
14
+ end
15
+
16
+ # Output a message to stdout if either verbose or alert was specified.
17
+ def self.alert(msg)
18
+ $stdout.puts msg if $opts.verbose or $opts.alert
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,112 @@
1
+ require 'optparse'
2
+ require 'ostruct'
3
+ require 'singleton'
4
+
5
+ module DNSOMatic
6
+ # A class to handle option parsing for the dnsomatic client.
7
+ # It acts as a singleton and will recreate its internal representation
8
+ # of the options each time parse(ARGS) is called.
9
+ class Opts
10
+ @@opts = OpenStruct.new
11
+
12
+ include Singleton
13
+
14
+ # No arguments, but sets the internal options to the defaults.
15
+ def initialize
16
+ setdefaults()
17
+ end
18
+
19
+ # Parse and array of arguments and set the interal options. Typically
20
+ # ARGV is passed, but that is not required. Any array of strings that
21
+ # look like arguments will work (makes testing easier).
22
+ def parse(args)
23
+ setdefaults()
24
+ begin
25
+ opts = OptionParser.new do |o|
26
+ o.on('-m', '--minimum SEC', 'The minimum time between updates (def: 30m)') do |i|
27
+ @@opts.minimum = i.to_i
28
+ end
29
+
30
+ o.on('-M', '--maximum SEC', 'The maximum time between updates (def: 15d)') do |i|
31
+ @@opts.maximum = i.to_i
32
+ end
33
+ #
34
+ #making this an option (off by default) means we can operate
35
+ #completely silently by default.
36
+ o.on('-a', '--alert', 'Emit an alert if the IP is updated') do |a|
37
+ @@opts.alert = true
38
+ end
39
+
40
+ o.on('-n', '--name NAME', 'Only update host stanza NAME') do |n|
41
+ @@opts.name = n
42
+ end
43
+
44
+ o.on('-d', '--display-config', 'Display the configuration and exit') do
45
+ @@opts.showcf = true
46
+ end
47
+
48
+ o.on('-c', '--config FILE', 'Use an alternate config file') do |f|
49
+ @@opts.cf = f
50
+ end
51
+
52
+ o.on('-f', '--force', 'Force an update, even if IP is unchanged') do
53
+ @@opts.force = true
54
+ end
55
+
56
+ o.on('-p', '--print', 'Output the update URLs. No action taken') do
57
+ @@opts.print = true
58
+ end
59
+
60
+ o.on('-v', '--verbose', 'Display runtime messages') do
61
+ @@opts.verbose = true
62
+ end
63
+
64
+ o.on('-V', '--version', 'Display version and exit') do
65
+ $stdout.puts DNSOMatic::VERSION
66
+ exit 0
67
+ end
68
+
69
+ o.on('-x', '--debug', 'Output additional info in error situations') do
70
+ @@opts.debug = true
71
+ end
72
+
73
+ o.on('-h', '--help', 'Display this help text') { puts o; exit; }
74
+ end
75
+ opts.parse!(args)
76
+
77
+ if args.size > 0
78
+ raise(DNSOMatic::Error, "Extra arguments given: #{args.join(', ')}")
79
+ end
80
+
81
+ rescue OptionParser::ParseError => e
82
+ msg = "Extra/Unknown arguments used:\n"
83
+ msg += "\t#{e.message}\n"
84
+ msg += "Remaining args: #{args.join(', ')}" if args.size > 0
85
+ raise(DNSOMatic::Error, msg)
86
+ end
87
+ end
88
+
89
+ # This is a simple wrapper that passes method calls to our internal option
90
+ # store (an openstruct) so that a client can call opts.alert and get the
91
+ # value from the @@opts.alert variable.
92
+ def method_missing(meth, *args)
93
+ @@opts.send(meth)
94
+ end
95
+
96
+ private
97
+ # A quick way to set all of our preferred defaults
98
+ def setdefaults
99
+ @@opts.name = nil
100
+ @@opts.cf = nil
101
+ @@opts.showcf = false
102
+ @@opts.force = false
103
+ @@opts.print = false
104
+ @@opts.verbose = false
105
+ @@opts.minimum = 1800 #30 minutes
106
+ @@opts.maximum = 1296000 #15 days
107
+ @@opts.debug = false
108
+ @@opts.alert = false
109
+ end
110
+
111
+ end
112
+ end
@@ -0,0 +1,79 @@
1
+
2
+ module DNSOMatic
3
+ # This is the primary interface used to perform updates of IP information
4
+ # to the DNS-o-Matic service.
5
+ class Updater
6
+ # Initialize and updater object with a set of config options. config is
7
+ # a hash that must contain the following keys:
8
+ # * hostname: a string representing the hostname to be updated
9
+ # * mx: a string representing a valid MX for hostname
10
+ # * backmx: a string representing a secondary MX for hostname
11
+ # * username: the username with rights to update hostname
12
+ # * password: the corresponding password
13
+ # * webipfetchurl: a url that returns the IP of the system requesting the it
14
+ # * offline: determines whether 'offline' mode should be used.
15
+ def initialize(config)
16
+ @config = config
17
+ #use a cache in case other host stanzas have already looked up
18
+ #our ip using the same remote agent.
19
+ @lookup = DNSOMatic::IPLookup.instance
20
+ end
21
+
22
+ # Build and send the url that is passed to the DNS-o-Matic system to
23
+ # request an update for the hostname provided in the configuration.
24
+ # By default, the we'll honour the changed? status of the IPStatus object
25
+ # returned to us, but if force is set to true, we'll send the update
26
+ # request anyway.
27
+ def update(force = false) url = upd_url()
28
+
29
+ if !@ipstatus.changed? and !force
30
+ Logger::log("No change in IP detected for #{@config['hostname']}. Not updating.")
31
+ else
32
+ Logger::alert("Updating IP for #{@config['hostname']} to #{@ipstatus.ip}.")
33
+ update = DNSOMatic::http_fetch(url)
34
+
35
+ if !update.match(/^good\s+#{@ipstatus.ip}$/)
36
+ msg = "Error updating host definition for #{@config['hostname']}\n"
37
+ msg += "Results:\n#{update}\n"
38
+ msg += "Error codes at: https://www.dnsomatic.com/wiki/api"
39
+ raise(DNSOMatic::Error, msg)
40
+ end
41
+ end
42
+
43
+ true
44
+ end
45
+
46
+ # A simple wrapper around update that sets force to true. The forceful
47
+ # nature of the update is logged if verbosity is enabled.
48
+ def update!
49
+ Logger::log("Forcing update at user request.")
50
+ update(true)
51
+ end
52
+
53
+ # This is used to simply return the url that would be sent to DNS-o-Matic.
54
+ # Using this is an easy way to build a list of update URL's that could
55
+ # be directed elsewhere (a log, or wget, etc).
56
+ def to_s
57
+ upd_url
58
+ end
59
+
60
+ private
61
+ def upd_url
62
+ opt_params = %w(wildcard mx backmx offline)
63
+ u = @config['username']
64
+ p = @config['password']
65
+ @ipstatus = @lookup.ip_from_url(@config['webipfetchurl'])
66
+
67
+ #we'll use nil as a key from the ip lookup to determine that the ip
68
+ #hasn't changed, so no update is required.
69
+ url = "https://#{u}:#{p}@updates.dnsomatic.com/nic/update?"
70
+ name_ip = "hostname=#{@config['hostname']}&myip=#{@ipstatus.ip}"
71
+ url += opt_params.inject(name_ip) do |params, curp|
72
+ val = @config[curp]
73
+ next if val.eql?('') or val.nil?
74
+ params += "&#{curp}=#{val}"
75
+ end
76
+ url
77
+ end
78
+ end
79
+ end #end module
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/ruby -w
2
+ #
3
+ require 'test/unit'
4
+
5
+ Dir.glob("test*rb") do |f|
6
+ require "#{f.sub(/\.rb/, '')}"
7
+ end
8
+
9
+
@@ -0,0 +1,59 @@
1
+ #!/usr/bin/ruby -w
2
+ $: << '../lib'
3
+
4
+ require 'test/unit'
5
+ require 'ostruct'
6
+
7
+ require 'dnsomatic'
8
+
9
+ class TestIPLookup < Test::Unit::TestCase
10
+ def setup
11
+ $opts = DNSOMatic::Opts.instance
12
+ $fp = File.join('/tmp', 'dnsomatic.testcache-' + Process.pid.to_s)
13
+ $iplookup = DNSOMatic::IPLookup.instance
14
+ $iplookup.setcachefile($fp)
15
+ $iplookup.persist = false
16
+ $local = 'http://benandwen.net/~bwalton/ip_lookup.php'
17
+ $local_ip = %x{wget -q -O - http://benandwen.net/~bwalton/ip_lookup.php}
18
+ $random = 'http://benandwen.net/~bwalton/ip_rand.php'
19
+ end
20
+
21
+ def test_cache_works
22
+ stat = $iplookup.ip_from_url($local)
23
+ assert_equal(DNSOMatic::IPStatus::CHANGED, stat.changed?)
24
+ assert_equal($local_ip, stat.ip)
25
+ stat.update
26
+ assert_equal(DNSOMatic::IPStatus::UNCHANGED, stat.changed?)
27
+ end
28
+
29
+ def test_different_source_returns_new_val
30
+ stat = $iplookup.ip_from_url($random)
31
+ assert_equal(DNSOMatic::IPStatus::CHANGED, stat.changed?)
32
+ end
33
+
34
+ def test_known_ip_change_still_prefers_cache
35
+ #ensure we have a long interval between polls, so we know we get cached ip
36
+ $opts.parse(%w(-m 1800))
37
+ stat = $iplookup.ip_from_url($random)
38
+ assert_equal(DNSOMatic::IPStatus::UNCHANGED, stat.changed?)
39
+ end
40
+
41
+ def test_expiration_with_known_change
42
+ stat = $iplookup.ip_from_url($random)
43
+ assert_equal(DNSOMatic::IPStatus::UNCHANGED, stat.changed?)
44
+ $opts.parse(%w(-m 0)) #change expiration to 2s.
45
+ sleep(1) #make sure we pass the expiration time we just set.
46
+ stat = $iplookup.ip_from_url($random)
47
+ assert_equal(DNSOMatic::IPStatus::CHANGED, stat.changed?)
48
+ end
49
+
50
+ def test_persist_stores_to_file
51
+ $iplookup.persist = true
52
+ stat = $iplookup.ip_from_url($random)
53
+ assert(File.exists?($fp), "${fp} doesn't exist when persist is enabled.")
54
+ end
55
+
56
+ def teardown
57
+ File.delete($fp) if File.exists?($fp)
58
+ end
59
+ end
@@ -0,0 +1,39 @@
1
+ #!/usr/bin/ruby -w
2
+ $: << '../lib'
3
+
4
+ require 'test/unit'
5
+ require 'ostruct'
6
+
7
+ require 'dnsomatic'
8
+
9
+ class TestIPStatus < Test::Unit::TestCase
10
+ def setup
11
+ $opts = DNSOMatic::Opts.instance
12
+ $opts.parse(%w(-m 0))
13
+ $local = 'http://benandwen.net/~bwalton/ip_lookup.php'
14
+ $local_ip = %x{wget -q -O - http://benandwen.net/~bwalton/ip_lookup.php}
15
+ $random = 'http://benandwen.net/~bwalton/ip_rand.php'
16
+ $non_lookup = 'http://benandwen.net/~bwalton/non_ip_lookup.txt'
17
+ end
18
+
19
+ def test_returns_right_ip
20
+ stat = DNSOMatic::IPStatus.new($local)
21
+ assert_equal($local_ip, stat.ip)
22
+ end
23
+
24
+ def test_returns_right_status
25
+ stat1 = DNSOMatic::IPStatus.new($local)
26
+ assert_equal(true, stat1.changed?)
27
+ stat1.update
28
+ assert_equal(false, stat1.changed?)
29
+
30
+ stat2 = DNSOMatic::IPStatus.new($random)
31
+ assert_equal(true, stat2.changed?)
32
+ stat2.update
33
+ assert_equal(true, stat2.changed?)
34
+ end
35
+
36
+ def test_raise_on_bad_args
37
+ assert_raise(DNSOMatic::Error) { DNSOMatic::IPStatus.new($non_lookup) }
38
+ end
39
+ end
@@ -0,0 +1,78 @@
1
+ #!/usr/bin/ruby -w
2
+ $: << '../lib'
3
+
4
+ require 'test/unit'
5
+ require 'ostruct'
6
+
7
+ require 'dnsomatic'
8
+
9
+ class TestOpts < Test::Unit::TestCase
10
+ def setup
11
+ $opts = DNSOMatic::Opts.instance
12
+ end
13
+
14
+ def test_set_verbose
15
+ $opts.parse(%w(-v))
16
+ assert($opts.verbose)
17
+ $opts.parse(%w(--verbose))
18
+ assert($opts.verbose)
19
+ end
20
+
21
+ def test_set_minimum
22
+ $opts.parse(%w(-m 10))
23
+ assert_equal(10, $opts.minimum)
24
+ $opts.parse(%w(--minimum 15))
25
+ assert_equal(15, $opts.minimum)
26
+ end
27
+
28
+ def test_set_maximum
29
+ $opts.parse(%w(-M 10))
30
+ assert_equal(10, $opts.maximum)
31
+ $opts.parse(%w(--maximum 15))
32
+ assert_equal(15, $opts.maximum)
33
+ end
34
+
35
+ def test_set_alert
36
+ $opts.parse(%w(-a))
37
+ assert($opts.alert)
38
+ $opts.parse(%w(--alert))
39
+ assert($opts.alert)
40
+ end
41
+
42
+ def test_set_name
43
+ $opts.parse(%w(-n testcase))
44
+ assert_equal('testcase', $opts.name)
45
+ $opts.parse(%w(--name testcase2))
46
+ assert_equal('testcase2', $opts.name)
47
+ end
48
+
49
+ def test_set_display_config
50
+ $opts.parse(%w(-d))
51
+ assert($opts.showcf)
52
+ $opts.parse(%w(--display-config))
53
+ assert($opts.showcf)
54
+ end
55
+
56
+ def test_set_config_file
57
+ $opts.parse(%w(-c /tmp/dnsomatic))
58
+ assert_equal('/tmp/dnsomatic', $opts.cf)
59
+ $opts.parse(%w(--config /tmp/dnsomatic2))
60
+ assert_equal('/tmp/dnsomatic2', $opts.cf)
61
+ end
62
+
63
+ def test_set_force
64
+ $opts.parse(%w(-f))
65
+ assert($opts.force)
66
+ $opts.parse(%w(--force))
67
+ assert($opts.force)
68
+ end
69
+
70
+ def test_extra_opts_raise_exception
71
+ assert_raise(DNSOMatic::Error) { $opts.parse(%w(--badoption)) }
72
+ end
73
+
74
+ def test_extra_args_raise_exception
75
+ assert_raise(DNSOMatic::Error) { $opts.parse(%w(somearg)) }
76
+ end
77
+
78
+ end
@@ -0,0 +1,33 @@
1
+ #!/usr/bin/ruby -w
2
+ $: << '../lib'
3
+
4
+ require 'test/unit'
5
+ require 'ostruct'
6
+
7
+ require 'dnsomatic'
8
+
9
+ class TestUpdater < Test::Unit::TestCase
10
+ def setup
11
+ $opts = DNSOMatic::Opts.instance
12
+ $fp = File.join('/tmp', 'dnsomatic.testcache-' + Process.pid.to_s)
13
+ $iplookup = DNSOMatic::IPLookup.instance
14
+ $iplookup.setcachefile($fp)
15
+ $iplookup.persist = false
16
+ $local = 'http://benandwen.net/~bwalton/ip_lookup.php'
17
+ $local_ip = %x{wget -q -O - http://benandwen.net/~bwalton/ip_lookup.php}
18
+ $random = 'http://benandwen.net/~bwalton/ip_rand.php'
19
+ end
20
+
21
+ def giveconf(u, p, mx, bmx, w, o, wf = $local, hn = 'all.dnsomatic.com')
22
+ { 'username' => u, 'password' => p, 'mx' => mx, 'backmx' => bmx,
23
+ 'wildcard' => w, 'offline' => o , 'webipfetchurl' => wf,
24
+ 'hostname' => hn}
25
+ end
26
+
27
+ def test_config_taken
28
+ c = giveconf('user', 'mypass', 'NOCHG', 'NOCHG', 'NOCHG', 'NOCHG')
29
+ url = "https://user:mypass@updates.dnsomatic.com/nic/update?hostname=all.dnsomatic.com&myip=#{$local_ip}&wildcard=NOCHG&mx=NOCHG&backmx=NOCHG&offline=NOCHG"
30
+ u = DNSOMatic::Updater.new(c)
31
+ assert_equal(url, u.to_s)
32
+ end
33
+ end
metadata ADDED
@@ -0,0 +1,65 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: dnsomatic
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - Ben Walton
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2008-08-26 00:00:00 -04:00
13
+ default_executable: dnsomatic
14
+ dependencies: []
15
+
16
+ description:
17
+ email: bdwalton@gmail.com
18
+ executables:
19
+ - dnsomatic
20
+ extensions: []
21
+
22
+ extra_rdoc_files: []
23
+
24
+ files:
25
+ - bin/dnsomatic
26
+ - lib/dnsomatic
27
+ - lib/dnsomatic/updater.rb
28
+ - lib/dnsomatic/logger.rb
29
+ - lib/dnsomatic/iplookup.rb
30
+ - lib/dnsomatic/opts.rb
31
+ - lib/dnsomatic/config.rb
32
+ - lib/dnsomatic.rb
33
+ - tests/test_ipstatus.rb
34
+ - tests/test_opts.rb
35
+ - tests/test_updater.rb
36
+ - tests/all_tests.rb
37
+ - tests/test_iplookup.rb
38
+ has_rdoc: true
39
+ homepage: http://rubyforge.org/projects/dnsomatic
40
+ post_install_message:
41
+ rdoc_options:
42
+ - -x tests/
43
+ require_paths:
44
+ - lib
45
+ required_ruby_version: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - ">="
48
+ - !ruby/object:Gem::Version
49
+ version: "0"
50
+ version:
51
+ required_rubygems_version: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ version: "0"
56
+ version:
57
+ requirements: []
58
+
59
+ rubyforge_project: http://rubyforge.org/projects/dnsomatic
60
+ rubygems_version: 1.1.1
61
+ signing_key:
62
+ specification_version: 2
63
+ summary: A DNS-O-Matic Update Client
64
+ test_files:
65
+ - tests/all_tests.rb