dnsign 0.0.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.
- checksums.yaml +7 -0
- data/.gitignore +14 -0
- data/.rspec +2 -0
- data/.travis.yml +8 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +34 -0
- data/Rakefile +2 -0
- data/bin/dnsign +18 -0
- data/config/example.yml +4 -0
- data/dnsign.gemspec +30 -0
- data/lib/dnsign.rb +15 -0
- data/lib/dnsign/config_loader.rb +49 -0
- data/lib/dnsign/dns_service.rb +33 -0
- data/lib/dnsign/dns_services/digital_ocean.rb +63 -0
- data/lib/dnsign/dns_services/linode.rb +64 -0
- data/lib/dnsign/error.rb +9 -0
- data/lib/dnsign/ip_resolver.rb +21 -0
- data/lib/dnsign/ip_resolvers/ip_info_io.rb +23 -0
- data/lib/dnsign/resolve_update_loop.rb +70 -0
- data/lib/dnsign/version.rb +3 -0
- data/lib/vendor/droplet_kit.rb +28 -0
- data/spec/dns_services/digital_ocean_spec.rb +43 -0
- data/spec/dns_services/linode_spec.rb +43 -0
- data/spec/fixtures/vcr_cassettes/digital_ocean_create_domain_record.yml +129 -0
- data/spec/fixtures/vcr_cassettes/digital_ocean_read_domain_record.yml +65 -0
- data/spec/fixtures/vcr_cassettes/digital_ocean_update_domain_record.yml +129 -0
- data/spec/fixtures/vcr_cassettes/ip_resolver_ip_info_io_response.yml +48 -0
- data/spec/fixtures/vcr_cassettes/linode_create_domain_record.yml +159 -0
- data/spec/fixtures/vcr_cassettes/linode_read_domain_record.yml +81 -0
- data/spec/fixtures/vcr_cassettes/linode_update_domain_record.yml +120 -0
- data/spec/ip_resolver_spec.rb +13 -0
- data/spec/resolve_update_loop_spec.rb +25 -0
- data/spec/spec_helper.rb +31 -0
- metadata +205 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 85311274d2708899e43aa9ccdbf039c29efe8354
|
4
|
+
data.tar.gz: efd33c67131f96eb5910af36346b8594a4481530
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: a5cec85f197d8c2cb29397c948384dbc9de5c12c32c5f9390770ffcb3545750c1dbb84bfae6fe52bfc644631c68286d1be5cd8a9052dfbe618079d56666f684d
|
7
|
+
data.tar.gz: 1c57245b2c64fd5970f3168fdffe7ae2a692372e7f9ff0eea97fbc733f3f61f2730b7202a69c10a476ccc10e5a84f97ea096039c5223fa2be41af76b61468973
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2015 Veljko Dragsic
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
[](https://travis-ci.org/vdragsic/dnsign)
|
2
|
+
|
3
|
+
# Dnsign
|
4
|
+
|
5
|
+
Dnsign is a dynamic DNS updater.
|
6
|
+
|
7
|
+
It's a simple command line tool that resolves your public IP address and accordingly updates DNS record on one of the currently supported (Linode, DigitalOcean) DNS services.
|
8
|
+
|
9
|
+
## Installation
|
10
|
+
|
11
|
+
`gem install dnsign`
|
12
|
+
|
13
|
+
## Usage
|
14
|
+
|
15
|
+
`dnsign --config /path/to/config.yml`
|
16
|
+
|
17
|
+
Example config:
|
18
|
+
|
19
|
+
```
|
20
|
+
dns_service: 'DigitalOcean' # [DigitalOcean|Linode]
|
21
|
+
dns_token: 'your access token from DNS service'
|
22
|
+
domain: 'foobar.example.com'
|
23
|
+
interval: 300 # refresh interval in seconds, defaults to 5 minutes
|
24
|
+
```
|
25
|
+
|
26
|
+
It's important that you already have domain on DNS service. Dnsign will periodically check your public IP and try to update or add DNS record to your domain.
|
27
|
+
|
28
|
+
For example above to work, there already must be `example.com` domain on DNS service and Dnsign will try to add/update `A` record `foobar` with currently public IP of the host it's running on.
|
29
|
+
|
30
|
+
## Adding support for other DNS services
|
31
|
+
|
32
|
+
It should be not to hard to add support for the other DNS services.
|
33
|
+
|
34
|
+
Just inherit `Dnsign::DnsService` and implement `update_ip`, `retrieve_ip` and optionally initializer methods. For example take a look at one of the existing services.
|
data/Rakefile
ADDED
data/bin/dnsign
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'dnsign'
|
4
|
+
|
5
|
+
def config
|
6
|
+
@config ||= Dnsign::ConfigLoader.parse_and_load ARGV
|
7
|
+
end
|
8
|
+
|
9
|
+
def dns_service
|
10
|
+
@dns_service ||= Dnsign::DnsService.create_from_name config[:dns_service], access_token: config[:dns_token]
|
11
|
+
rescue Dnsign::Error::UnsupportedDnsService => e
|
12
|
+
puts e
|
13
|
+
exit(1)
|
14
|
+
end
|
15
|
+
|
16
|
+
loop = Dnsign::ResolveUpdateLoop.new(config[:domain], dns_service, Dnsign::IpResolver.new, config)
|
17
|
+
loop.update # don't wait for the first tick
|
18
|
+
loop.kickoff
|
data/config/example.yml
ADDED
data/dnsign.gemspec
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'dnsign/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "dnsign"
|
8
|
+
spec.version = Dnsign::VERSION
|
9
|
+
spec.authors = ["Veljko Dragsic"]
|
10
|
+
spec.email = ["veljko@floatingpoint.io"]
|
11
|
+
spec.summary = %q{Dynamic DNS updater}
|
12
|
+
spec.description = "Simple command line tool that resolves your public IP address and accordingly updates DNS record on one of the currently supported (Linode, DigitalOcean) DNS services."
|
13
|
+
spec.homepage = "http://github.com/vdragsic/dnsign"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0")
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_dependency "timers", "~> 4.0"
|
22
|
+
spec.add_dependency "droplet_kit", "~> 1.2"
|
23
|
+
spec.add_dependency "linode", "~> 0.8"
|
24
|
+
|
25
|
+
spec.add_development_dependency "bundler", "~> 1.6"
|
26
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
27
|
+
spec.add_development_dependency "rspec", "~> 3.2"
|
28
|
+
spec.add_development_dependency "webmock", "~> 1.21"
|
29
|
+
spec.add_development_dependency "vcr", "~> 2.9"
|
30
|
+
end
|
data/lib/dnsign.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'dnsign/version'
|
3
|
+
require 'dnsign/error'
|
4
|
+
require 'dnsign/dns_service'
|
5
|
+
require 'dnsign/dns_services/linode'
|
6
|
+
require 'dnsign/dns_services/digital_ocean'
|
7
|
+
require 'dnsign/ip_resolver'
|
8
|
+
require 'dnsign/config_loader'
|
9
|
+
require 'dnsign/resolve_update_loop'
|
10
|
+
require 'json'
|
11
|
+
require 'yaml'
|
12
|
+
require 'net/http'
|
13
|
+
|
14
|
+
module Dnsign
|
15
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
require 'yaml'
|
3
|
+
|
4
|
+
module Dnsign
|
5
|
+
class ConfigLoader
|
6
|
+
|
7
|
+
Config = Struct.new :path
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
@config = Config.new
|
11
|
+
end
|
12
|
+
|
13
|
+
def parse(params)
|
14
|
+
opt_parser = OptionParser.new do |opts|
|
15
|
+
opts.banner = "Usage: ruby dyndns.rb [options]"
|
16
|
+
|
17
|
+
opts.on("-cCONFIG", "--config=CONFIG", "Path to config file") do |c|
|
18
|
+
@config.path = c
|
19
|
+
end
|
20
|
+
|
21
|
+
opts.on("-h", "--help", "Prints this help") do
|
22
|
+
puts opts
|
23
|
+
exit
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
opt_parser.parse! params
|
28
|
+
|
29
|
+
return @config
|
30
|
+
end
|
31
|
+
|
32
|
+
def load(path)
|
33
|
+
config = YAML.load_file path
|
34
|
+
|
35
|
+
# symbolize keys
|
36
|
+
config.reduce({}) do |acc, (k,v)|
|
37
|
+
acc[k.to_sym] = v
|
38
|
+
acc
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.parse_and_load(params)
|
43
|
+
loader = self.new
|
44
|
+
config = loader.parse params
|
45
|
+
loader.load config.path
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Dnsign
|
2
|
+
class DnsService
|
3
|
+
|
4
|
+
def initialize(opts={})
|
5
|
+
end
|
6
|
+
|
7
|
+
def update_ip(fqdn, ip)
|
8
|
+
raise NotImplementedError
|
9
|
+
end
|
10
|
+
|
11
|
+
def retrieve_ip(fqdn)
|
12
|
+
raise NotImplementedError
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.create_from_name(service_name, opts={})
|
16
|
+
service_name = service_name.to_sym
|
17
|
+
|
18
|
+
if DnsServices.constants.include? service_name
|
19
|
+
DnsServices.const_get(service_name).new opts
|
20
|
+
else
|
21
|
+
fail Error::UnsupportedDnsService,
|
22
|
+
"DNS Service #{service_name} is not supported, choose among #{DnsServices::Constants}"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
protected
|
27
|
+
|
28
|
+
def split_fqdn(fqdn)
|
29
|
+
result = /(.*)\.(.*\..*)/.match fqdn
|
30
|
+
[result[1], result[2]]
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'droplet_kit'
|
2
|
+
require 'dnsign/dns_service'
|
3
|
+
|
4
|
+
# monkey patch
|
5
|
+
require 'vendor/droplet_kit'
|
6
|
+
|
7
|
+
module DnsServices
|
8
|
+
class DigitalOcean < Dnsign::DnsService
|
9
|
+
|
10
|
+
def initialize(opts={})
|
11
|
+
@access_token = opts.fetch :access_token
|
12
|
+
end
|
13
|
+
|
14
|
+
def update_ip(fqdn, ip)
|
15
|
+
name, domain = split_fqdn fqdn
|
16
|
+
|
17
|
+
if existing = fetch_record_by_name(domain, name)
|
18
|
+
handle_record_response update_record(existing.id, domain, name, ip)
|
19
|
+
else
|
20
|
+
handle_record_response create_record(domain, name, ip)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def retrieve_ip(fqdn)
|
25
|
+
name, domain = split_fqdn fqdn
|
26
|
+
|
27
|
+
if record = fetch_record_by_name(domain, name)
|
28
|
+
record.data
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def client
|
35
|
+
@client ||= DropletKit::Client.new access_token: @access_token
|
36
|
+
end
|
37
|
+
|
38
|
+
def handle_record_response(record)
|
39
|
+
record.data # ip
|
40
|
+
end
|
41
|
+
|
42
|
+
def fetch_record_by_name(domain, record_name)
|
43
|
+
# todo: handle non-existing domains
|
44
|
+
|
45
|
+
client
|
46
|
+
.domain_records
|
47
|
+
.all(for_domain: domain)
|
48
|
+
.select {|r| r.name == record_name}
|
49
|
+
.first
|
50
|
+
end
|
51
|
+
|
52
|
+
def update_record(id, domain, record_name, ip)
|
53
|
+
record = DropletKit::DomainRecord.new(name: record_name, data: ip)
|
54
|
+
client.domain_records.update(record, for_domain: domain, id: id)
|
55
|
+
end
|
56
|
+
|
57
|
+
def create_record(domain, record_name, ip)
|
58
|
+
record = DropletKit::DomainRecord.new(type: 'A', name: record_name, data: ip)
|
59
|
+
client.domain_records.create(record, for_domain: domain)
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
require 'linode'
|
2
|
+
require 'dnsign/dns_service'
|
3
|
+
|
4
|
+
module DnsServices
|
5
|
+
class Linode < Dnsign::DnsService
|
6
|
+
|
7
|
+
def initialize(opts={})
|
8
|
+
@access_token = opts.fetch :access_token
|
9
|
+
end
|
10
|
+
|
11
|
+
def update_ip(fqdn, ip)
|
12
|
+
record_name, domain_name = split_fqdn fqdn
|
13
|
+
|
14
|
+
if record = fetch_record(domain_name, record_name)
|
15
|
+
response = update_record(record.domainid, record.resourceid, record_name, ip)
|
16
|
+
handle_record_response response, ip
|
17
|
+
else
|
18
|
+
domain = fetch_domain(domain_name)
|
19
|
+
response = create_record(domain.domainid, record_name, ip)
|
20
|
+
handle_record_response response, ip
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def retrieve_ip(fqdn)
|
25
|
+
record_name, domain_name = split_fqdn fqdn
|
26
|
+
|
27
|
+
if record = fetch_record(domain_name, record_name)
|
28
|
+
record.target
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def client
|
35
|
+
@client ||= ::Linode.new api_key: @access_token
|
36
|
+
end
|
37
|
+
|
38
|
+
def handle_record_response(response, ip)
|
39
|
+
# Linode API only returns resourceid on success
|
40
|
+
ip if response.resourceid
|
41
|
+
end
|
42
|
+
|
43
|
+
def fetch_domain(domain_name)
|
44
|
+
domain = client.domain.list.select {|d| d.domain == domain_name}.first
|
45
|
+
end
|
46
|
+
|
47
|
+
def fetch_record(domain_name, record_name)
|
48
|
+
domain = fetch_domain domain_name
|
49
|
+
client
|
50
|
+
.domain
|
51
|
+
.resource.list(DomainId: domain.domainid)
|
52
|
+
.select {|r| r.name == record_name}.first
|
53
|
+
end
|
54
|
+
|
55
|
+
def create_record(domain_id, record_name, ip)
|
56
|
+
client.domain.resource.create DomainId: domain_id, Type: 'A', Name: record_name, Target: ip
|
57
|
+
end
|
58
|
+
|
59
|
+
def update_record(domain_id, record_id, record_name, ip)
|
60
|
+
client.domain.resource.update DomainId: domain_id, ResourceId: record_id, Name: record_name, Target: ip
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
64
|
+
end
|
data/lib/dnsign/error.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'dnsign/ip_resolvers/ip_info_io.rb'
|
2
|
+
|
3
|
+
module Dnsign
|
4
|
+
class IpResolver
|
5
|
+
|
6
|
+
def initialize(service=IpResolvers::IpInfoIo, opts={})
|
7
|
+
@resolver = service.new self
|
8
|
+
end
|
9
|
+
|
10
|
+
def resolve
|
11
|
+
@resolver.fetch
|
12
|
+
rescue => e
|
13
|
+
puts "#{e.class}: #{e.message}"
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.resolve(*args)
|
17
|
+
self.new(*args).resolve
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module IpResolvers
|
2
|
+
class IpInfoIo
|
3
|
+
|
4
|
+
def initialize(resolver)
|
5
|
+
@resolver = resolver
|
6
|
+
end
|
7
|
+
|
8
|
+
def fetch
|
9
|
+
uri = URI 'http://ipinfo.io/json'
|
10
|
+
|
11
|
+
res = ::Net::HTTP.get_response uri
|
12
|
+
|
13
|
+
if /2../.match res.code
|
14
|
+
data = JSON.parse res.body
|
15
|
+
data.fetch 'ip'
|
16
|
+
else
|
17
|
+
raise Error::InvalidResponseFromIpResolverService "Invalid response for #{self.class} with code: #{res.code}; body: #{res.body}"
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
end
|