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.
@@ -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
@@ -0,0 +1,14 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ *.bundle
11
+ *.so
12
+ *.o
13
+ *.a
14
+ mkmf.log
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --require spec_helper
@@ -0,0 +1,8 @@
1
+ language: ruby
2
+ script:
3
+ - "bundle exec rspec spec"
4
+ notifications:
5
+ recipients:
6
+ - veljko@floatingpoint.io
7
+ rvm:
8
+ - 2.1.2
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in dnsign.gemspec
4
+ gemspec
@@ -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.
@@ -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.
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+
@@ -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
@@ -0,0 +1,4 @@
1
+ dns_service: 'DigitalOcean' # [DigitalOcean|Linode]
2
+ dns_token: 'your access token from DNS service'
3
+ domain: 'foobar.example.com'
4
+ interval: 300 # refresh interval in seconds, defaults to 5 minutes
@@ -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
@@ -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
@@ -0,0 +1,9 @@
1
+ module Dnsign
2
+ module Error
3
+
4
+ class UnsupportedDnsService < StandardError; end
5
+ class DomainDoesNotExists < StandardError; end
6
+ class RecordDoesNotExists < StandardError; end
7
+ class InvalidResponseFromIpResolver < StandardError; end
8
+ end
9
+ end
@@ -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