dyndnsd 0.0.1 → 0.0.2

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.
@@ -2,9 +2,9 @@
2
2
  module Dyndnsd
3
3
  module Generator
4
4
  class Bind
5
- def initialize(config)
5
+ def initialize(domain, config)
6
+ @domain = domain
6
7
  @ttl = config['ttl']
7
- @origin = config['origin']
8
8
  @dns = config['dns']
9
9
  @email_addr = config['email_addr']
10
10
  end
@@ -12,13 +12,13 @@ module Dyndnsd
12
12
  def generate(zone)
13
13
  out = []
14
14
  out << "$TTL #{@ttl}"
15
- out << "$ORIGIN #{@origin}"
15
+ out << "$ORIGIN #{@domain}."
16
16
  out << ""
17
17
  out << "@ IN SOA #{@dns} #{@email_addr} ( #{zone['serial']} 3h 5m 1w 1h )"
18
18
  out << "@ IN NS #{@dns}"
19
19
  out << ""
20
20
  zone['hosts'].each do |hostname,ip|
21
- name = hostname.chomp('.' + @origin[0..-2])
21
+ name = hostname.chomp('.' + @domain)
22
22
  out << "#{name} IN A #{ip}"
23
23
  end
24
24
  out << ""
@@ -0,0 +1,21 @@
1
+
2
+ module Dyndnsd
3
+ module Responder
4
+ class DynDNSStyle
5
+ def response_for_error(state)
6
+ # general http errors
7
+ return [405, {"Content-Type" => "text/plain"}, ["Method Not Allowed"]] if state == :method_forbidden
8
+ return [404, {"Content-Type" => "text/plain"}, ["Not Found"]] if state == :not_found
9
+ # specific errors
10
+ return [200, {"Content-Type" => "text/plain"}, ["notfqdn"]] if state == :hostname_missing
11
+ return [200, {"Content-Type" => "text/plain"}, ["nohost"]] if state == :host_forbidden
12
+ return [200, {"Content-Type" => "text/plain"}, ["notfqdn"]] if state == :hostname_malformed
13
+ end
14
+
15
+ def response_for_changes(states, ip)
16
+ body = states.map { |state| "#{state} #{ip}" }.join("\n")
17
+ return [200, {"Content-Type" => "text/plain"}, [body]]
18
+ end
19
+ end
20
+ end
21
+ end
@@ -2,7 +2,7 @@
2
2
  module Dyndnsd
3
3
  module Responder
4
4
  class RestStyle
5
- def response_for(state)
5
+ def response_for_error(state)
6
6
  # general http errors
7
7
  return [405, {"Content-Type" => "text/plain"}, ["Method Not Allowed"]] if state == :method_forbidden
8
8
  return [404, {"Content-Type" => "text/plain"}, ["Not Found"]] if state == :not_found
@@ -10,9 +10,11 @@ module Dyndnsd
10
10
  return [422, {"Content-Type" => "text/plain"}, ["Hostname missing"]] if state == :hostname_missing
11
11
  return [403, {"Content-Type" => "text/plain"}, ["Forbidden"]] if state == :host_forbidden
12
12
  return [422, {"Content-Type" => "text/plain"}, ["Hostname malformed"]] if state == :hostname_malformed
13
- # OKs
14
- return [200, {"Content-Type" => "text/plain"}, ["Good"]] if state == :good
15
- return [200, {"Content-Type" => "text/plain"}, ["No change"]] if state == :nochg
13
+ end
14
+
15
+ def response_for_changes(states, ip)
16
+ body = states.map { |state| state == :good ? "Changed to #{ip}" : "No change needed for #{ip}" }.join("\n")
17
+ return [200, {"Content-Type" => "text/plain"}, [body]]
16
18
  end
17
19
  end
18
20
  end
@@ -2,10 +2,10 @@
2
2
  module Dyndnsd
3
3
  module Updater
4
4
  class CommandWithBindZone
5
- def initialize(config)
5
+ def initialize(domain, config)
6
6
  @zone_file = config['zone_file']
7
7
  @command = config['command']
8
- @generator = Generator::Bind.new(config)
8
+ @generator = Generator::Bind.new(domain, config)
9
9
  end
10
10
 
11
11
  def update(zone)
@@ -1,4 +1,4 @@
1
1
 
2
2
  module Dyndnsd
3
- VERSION = "0.0.1"
3
+ VERSION = "0.0.2"
4
4
  end
data/lib/dyndnsd.rb CHANGED
@@ -3,10 +3,12 @@
3
3
  require 'logger'
4
4
  require 'ipaddr'
5
5
  require 'json'
6
+ require 'yaml'
6
7
  require 'rack'
7
8
 
8
9
  require 'dyndnsd/generator/bind'
9
10
  require 'dyndnsd/updater/command_with_bind_zone'
11
+ require 'dyndnsd/responder/dyndns_style'
10
12
  require 'dyndnsd/responder/rest_style'
11
13
  require 'dyndnsd/database'
12
14
  require 'dyndnsd/version'
@@ -29,6 +31,7 @@ module Dyndnsd
29
31
  class Daemon
30
32
  def initialize(config, db, updater, responder)
31
33
  @users = config['users']
34
+ @domain = config['domain']
32
35
  @db = db
33
36
  @updater = updater
34
37
  @responder = responder
@@ -43,22 +46,34 @@ module Dyndnsd
43
46
  @updater.update(@db)
44
47
  end
45
48
 
49
+ def is_fqdn_valid?(hostname)
50
+ return false if hostname.length < @domain.length + 2
51
+ return false if not hostname.end_with?(@domain)
52
+ name = hostname.chomp(@domain)
53
+ return false if not name.match(/^[a-zA-Z0-9_-]+\.$/)
54
+ true
55
+ end
56
+
46
57
  def call(env)
47
- return @responder.response_for(:method_forbidden) if env["REQUEST_METHOD"] != "GET"
48
- return @responder.response_for(:not_found) if env["PATH_INFO"] != "/nic/update"
58
+ return @responder.response_for_error(:method_forbidden) if env["REQUEST_METHOD"] != "GET"
59
+ return @responder.response_for_error(:not_found) if env["PATH_INFO"] != "/nic/update"
49
60
 
50
61
  params = Rack::Utils.parse_query(env["QUERY_STRING"])
51
62
 
52
- return @responder.response_for(:hostname_missing) if not params["hostname"]
63
+ return @responder.response_for_error(:hostname_missing) if not params["hostname"]
53
64
 
54
- hostname = params["hostname"]
65
+ hostnames = params["hostname"].split(',')
55
66
 
56
- # Check if hostname(s) match rules
57
- #return @responder.response_for(:hostname_malformed) if XY
67
+ # Check if hostname match rules
68
+ hostnames.each do |hostname|
69
+ return @responder.response_for_error(:hostname_malformed) if not is_fqdn_valid?(hostname)
70
+ end
58
71
 
59
72
  user = env["REMOTE_USER"]
60
73
 
61
- return @responder.response_for(:host_forbidden) if not @users[user]['hosts'].include? hostname
74
+ hostnames.each do |hostname|
75
+ return @responder.response_for_error(:host_forbidden) if not @users[user]['hosts'].include? hostname
76
+ end
62
77
 
63
78
  # no myip?
64
79
  if not params["myip"]
@@ -74,16 +89,23 @@ module Dyndnsd
74
89
 
75
90
  myip = params["myip"]
76
91
 
77
- @db['hosts'][hostname] = myip
92
+ changes = []
93
+ hostnames.each do |hostname|
94
+ if (not @db['hosts'].include? hostname) or (@db['hosts'][hostname] != myip)
95
+ changes << :good
96
+ @db['hosts'][hostname] = myip
97
+ else
98
+ changes << :nochg
99
+ end
100
+ end
78
101
 
79
102
  if @db.changed?
80
103
  @db['serial'] += 1
81
104
  @db.save
82
105
  update
83
- return @responder.response_for(:good)
84
106
  end
85
107
 
86
- @responder.response_for(:nochg)
108
+ @responder.response_for_changes(changes, myip)
87
109
  end
88
110
 
89
111
  def self.run!
@@ -105,11 +127,11 @@ module Dyndnsd
105
127
  Dyndnsd.logger.info "DynDNSd version #{Dyndnsd::VERSION}"
106
128
  Dyndnsd.logger.info "Using config file #{config_file}"
107
129
 
108
- config = JSON.load(File.open(config_file, 'r') { |f| f.read })
130
+ config = YAML::load(File.open(config_file, 'r') { |f| f.read })
109
131
 
110
- db = Database.new(config['db_file'])
111
- updater = Updater::CommandWithBindZone.new(config['updater_params']) if config['updater'] == 'command_with_bind_zone'
112
- responder = Responder::RestStyle.new
132
+ db = Database.new(config['db'])
133
+ updater = Updater::CommandWithBindZone.new(config['domain'], config['updater']['params']) if config['updater']['name'] == 'command_with_bind_zone'
134
+ responder = Responder::DynDNSStyle.new
113
135
 
114
136
  app = Daemon.new(config, db, updater, responder)
115
137
  app = Rack::Auth::Basic.new(app, "DynDNS") do |user,pass|
data/spec/daemon_spec.rb CHANGED
@@ -5,16 +5,17 @@ describe Dyndnsd::Daemon do
5
5
 
6
6
  def app
7
7
  config = {
8
+ 'domain' => 'example.org',
8
9
  'users' => {
9
10
  'test' => {
10
11
  'password' => 'secret',
11
- 'hosts' => ['foo.example.org']
12
+ 'hosts' => ['foo.example.org', 'bar.example.org']
12
13
  }
13
14
  }
14
15
  }
15
16
  db = Dyndnsd::DummyDatabase.new({})
16
17
  updater = Dyndnsd::Updater::Dummy.new
17
- responder = Dyndnsd::Responder::RestStyle.new
18
+ responder = Dyndnsd::Responder::DynDNSStyle.new
18
19
  app = Dyndnsd::Daemon.new(config, db, updater, responder)
19
20
 
20
21
  Rack::Auth::Basic.new(app, "DynDNS") do |user,pass|
@@ -25,6 +26,9 @@ describe Dyndnsd::Daemon do
25
26
  it 'requires authentication' do
26
27
  get '/'
27
28
  last_response.status.should == 401
29
+
30
+ pending 'Need to find a way to add custom body on 401 responses'
31
+ last_response.should be_ok 'badauth'
28
32
  end
29
33
 
30
34
  it 'only supports GET requests' do
@@ -42,13 +46,54 @@ describe Dyndnsd::Daemon do
42
46
  it 'requires the hostname query parameter' do
43
47
  authorize 'test', 'secret'
44
48
  get '/nic/update'
45
- last_response.status.should == 422
49
+ last_response.should be_ok
50
+ last_response.body.should == 'notfqdn'
51
+ end
52
+
53
+ it 'supports multiple hostnames in request' do
54
+ authorize 'test', 'secret'
55
+ get '/nic/update?hostname=foo.example.org,bar.example.org&myip=1.2.3.4'
56
+ last_response.should be_ok
57
+ last_response.body.should == "good 1.2.3.4\ngood 1.2.3.4"
58
+ end
59
+
60
+ it 'rejects request if one hostname is invalid' do
61
+ authorize 'test', 'secret'
62
+
63
+ get '/nic/update?hostname=test'
64
+ last_response.should be_ok
65
+ last_response.body.should == 'notfqdn'
66
+
67
+ get '/nic/update?hostname=test.example.com'
68
+ last_response.should be_ok
69
+ last_response.body.should == 'notfqdn'
70
+
71
+ get '/nic/update?hostname=test.example.org.me'
72
+ last_response.should be_ok
73
+ last_response.body.should == 'notfqdn'
74
+
75
+ get '/nic/update?hostname=foo.test.example.org'
76
+ last_response.should be_ok
77
+ last_response.body.should == 'notfqdn'
78
+
79
+ get '/nic/update?hostname=in%20valid.example.org'
80
+ last_response.should be_ok
81
+ last_response.body.should == 'notfqdn'
82
+
83
+ get '/nic/update?hostname=valid.example.org,in.valid.example.org'
84
+ last_response.should be_ok
85
+ last_response.body.should == 'notfqdn'
46
86
  end
47
87
 
48
- it 'forbids changing hosts a user does not own' do
88
+ it 'rejects request if user does not own one hostname' do
49
89
  authorize 'test', 'secret'
50
90
  get '/nic/update?hostname=notmyhost.example.org'
51
- last_response.status.should == 403
91
+ last_response.should be_ok
92
+ last_response.body.should == 'nohost'
93
+
94
+ get '/nic/update?hostname=foo.example.org,notmyhost.example.org'
95
+ last_response.should be_ok
96
+ last_response.body.should == 'nohost'
52
97
  end
53
98
 
54
99
  it 'updates a host on change' do
@@ -57,9 +102,9 @@ describe Dyndnsd::Daemon do
57
102
  get '/nic/update?hostname=foo.example.org&myip=1.2.3.4'
58
103
  last_response.should be_ok
59
104
 
60
- get '/nic/update?hostname=foo.example.org&myip=1.2.3.400'
105
+ get '/nic/update?hostname=foo.example.org&myip=1.2.3.40'
61
106
  last_response.should be_ok
62
- last_response.body.should == 'Good'
107
+ last_response.body.should == 'good 1.2.3.40'
63
108
  end
64
109
 
65
110
  it 'returns no change' do
@@ -70,18 +115,25 @@ describe Dyndnsd::Daemon do
70
115
 
71
116
  get '/nic/update?hostname=foo.example.org&myip=1.2.3.4'
72
117
  last_response.should be_ok
73
- last_response.body.should == 'No change'
74
- end
75
-
76
- it 'forbids invalid hostnames' do
77
- pending
118
+ last_response.body.should == 'nochg 1.2.3.4'
78
119
  end
79
120
 
80
121
  it 'outputs status per hostname' do
81
- pending
122
+ authorize 'test', 'secret'
123
+
124
+ get '/nic/update?hostname=foo.example.org&myip=1.2.3.4'
125
+ last_response.should be_ok
126
+ last_response.body.should == 'good 1.2.3.4'
127
+
128
+ get '/nic/update?hostname=foo.example.org,bar.example.org&myip=1.2.3.4'
129
+ last_response.should be_ok
130
+ last_response.body.should == "nochg 1.2.3.4\ngood 1.2.3.4"
82
131
  end
83
132
 
84
- it 'supports multiple hostnames in request' do
85
- pending
133
+ it 'uses clients remote address if myip not specified' do
134
+ authorize 'test', 'secret'
135
+ get '/nic/update?hostname=foo.example.org'
136
+ last_response.should be_ok
137
+ last_response.body.should == 'good 127.0.0.1'
86
138
  end
87
139
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dyndnsd
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.2
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -13,7 +13,7 @@ date: 2013-04-27 00:00:00.000000000Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rack
16
- requirement: &79022390 !ruby/object:Gem::Requirement
16
+ requirement: &70281418775580 !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - ! '>='
@@ -21,10 +21,10 @@ dependencies:
21
21
  version: '0'
22
22
  type: :runtime
23
23
  prerelease: false
24
- version_requirements: *79022390
24
+ version_requirements: *70281418775580
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: bundler
27
- requirement: &79022140 !ruby/object:Gem::Requirement
27
+ requirement: &70281418775080 !ruby/object:Gem::Requirement
28
28
  none: false
29
29
  requirements:
30
30
  - - ~>
@@ -32,10 +32,10 @@ dependencies:
32
32
  version: '1.3'
33
33
  type: :development
34
34
  prerelease: false
35
- version_requirements: *79022140
35
+ version_requirements: *70281418775080
36
36
  - !ruby/object:Gem::Dependency
37
37
  name: rake
38
- requirement: &79021930 !ruby/object:Gem::Requirement
38
+ requirement: &70281418774660 !ruby/object:Gem::Requirement
39
39
  none: false
40
40
  requirements:
41
41
  - - ! '>='
@@ -43,10 +43,10 @@ dependencies:
43
43
  version: '0'
44
44
  type: :development
45
45
  prerelease: false
46
- version_requirements: *79021930
46
+ version_requirements: *70281418774660
47
47
  - !ruby/object:Gem::Dependency
48
48
  name: rspec
49
- requirement: &79021700 !ruby/object:Gem::Requirement
49
+ requirement: &70281418774200 !ruby/object:Gem::Requirement
50
50
  none: false
51
51
  requirements:
52
52
  - - ! '>='
@@ -54,10 +54,10 @@ dependencies:
54
54
  version: '0'
55
55
  type: :development
56
56
  prerelease: false
57
- version_requirements: *79021700
57
+ version_requirements: *70281418774200
58
58
  - !ruby/object:Gem::Dependency
59
59
  name: rack-test
60
- requirement: &79021490 !ruby/object:Gem::Requirement
60
+ requirement: &70281418773780 !ruby/object:Gem::Requirement
61
61
  none: false
62
62
  requirements:
63
63
  - - ! '>='
@@ -65,7 +65,7 @@ dependencies:
65
65
  version: '0'
66
66
  type: :development
67
67
  prerelease: false
68
- version_requirements: *79021490
68
+ version_requirements: *70281418773780
69
69
  description: A small, lightweight and extensible DynDNS server written with Ruby and
70
70
  Rack.
71
71
  email: chrnicolai@gmail.com
@@ -85,6 +85,7 @@ files:
85
85
  - lib/dyndnsd.rb
86
86
  - lib/dyndnsd/database.rb
87
87
  - lib/dyndnsd/generator/bind.rb
88
+ - lib/dyndnsd/responder/dyndns_style.rb
88
89
  - lib/dyndnsd/responder/rest_style.rb
89
90
  - lib/dyndnsd/updater/command_with_bind_zone.rb
90
91
  - lib/dyndnsd/version.rb