dyndnsd 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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