dnsign 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![Build Status](https://travis-ci.org/vdragsic/dnsign.svg?branch=master)](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
|