dnsomatic 0.1.1

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.
@@ -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