dyndnsd 1.4.0 → 1.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +0 -1
- data/README.md +11 -3
- data/dyndnsd.gemspec +1 -1
- data/lib/dyndnsd.rb +36 -29
- data/lib/dyndnsd/version.rb +1 -1
- data/spec/daemon_spec.rb +100 -65
- metadata +6 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 83264e341913c7b4dfc4e79402a60b3284a5bb41
|
4
|
+
data.tar.gz: 51af5408a701ca8829d3e73068c6bb198e3dc1b6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 810aca8e4360ec88e959f116460841638824174d87b841adc7396c3fa3226106ed673aa678a24e33554d24252e7c704300bdb59ccdb266661fe10723c7dff842
|
7
|
+
data.tar.gz: b056c351200e8ad50d120874ad77e466eb50bd27ee567722e5f9bf6bfe6c1481cf9c9f75de344d79c0855c102a45feb63566ddd7bda49581c5f72d4c338028ad
|
data/.travis.yml
CHANGED
data/README.md
CHANGED
@@ -6,11 +6,11 @@ A small, lightweight and extensible DynDNS server written with Ruby and Rack.
|
|
6
6
|
|
7
7
|
## Description
|
8
8
|
|
9
|
-
dyndnsd.rb
|
9
|
+
dyndnsd.rb aims to implement a small [DynDNS-compliant](http://dyn.com/support/developers/api/) server in Ruby supporting IPv4 and IPv6 addresses. It has an integrated user and hostname database in it's configuration file that is used for authentication and authorization. Besides talking the DynDNS protocol it is able to invoke an so-called *updater*, a small Ruby module that takes care of supplying the current host => ip mapping to a DNS server.
|
10
10
|
|
11
|
-
|
11
|
+
There is currently one updater shipped with dyndnsd.rb `command_with_bind_zone` that writes out a zone file in BIND syntax onto the current system and invokes a user-supplied command afterwards that is assumed to trigger the DNS server (not necessarily BIND since it's zone files are read by other DNS servers too) to reload it's zone configuration.
|
12
12
|
|
13
|
-
Because of the mechanisms used dyndnsd.rb is known to work only on
|
13
|
+
Because of the mechanisms used dyndnsd.rb is known to work only on \*nix systems.
|
14
14
|
|
15
15
|
## General Usage
|
16
16
|
|
@@ -112,6 +112,14 @@ where:
|
|
112
112
|
* HOSTNAMES is a required list of comma separated FQDNs (they all have to end with your config.yaml domain) the user wants to update
|
113
113
|
* MYIP is optional and the HTTP client's address will be used if missing
|
114
114
|
|
115
|
+
### IP address determination
|
116
|
+
|
117
|
+
The following rules apply:
|
118
|
+
|
119
|
+
* use any IP address provided via the myip parameter when present, or
|
120
|
+
* use any IP address provided via the X-Real-IP header e.g. when used behind HTTP reverse proxy such as nginx, or
|
121
|
+
* use any IP address used by the connecting HTTP client
|
122
|
+
|
115
123
|
### SSL, multiple listen ports
|
116
124
|
|
117
125
|
Use a webserver as a proxy to handle SSL and/or multiple listen addresses and ports. DynDNS.com provides HTTP on port 80 and 8245 and HTTPS on port 443.
|
data/dyndnsd.gemspec
CHANGED
@@ -21,7 +21,7 @@ Gem::Specification.new do |s|
|
|
21
21
|
s.executables = ['dyndnsd']
|
22
22
|
|
23
23
|
s.add_runtime_dependency 'rack', '~> 1.6'
|
24
|
-
s.add_runtime_dependency 'json'
|
24
|
+
s.add_runtime_dependency 'json', '~> 1.8'
|
25
25
|
s.add_runtime_dependency 'metriks'
|
26
26
|
|
27
27
|
s.add_development_dependency 'bundler', '~> 1.3'
|
data/lib/dyndnsd.rb
CHANGED
@@ -44,11 +44,11 @@ module Dyndnsd
|
|
44
44
|
@db['hosts'] ||= {}
|
45
45
|
(@db.save; update) if @db.changed?
|
46
46
|
end
|
47
|
-
|
47
|
+
|
48
48
|
def update
|
49
49
|
@updater.update(@db)
|
50
50
|
end
|
51
|
-
|
51
|
+
|
52
52
|
def is_fqdn_valid?(hostname)
|
53
53
|
return false if hostname.length < @domain.length + 2
|
54
54
|
return false if not hostname.end_with?(@domain)
|
@@ -56,45 +56,52 @@ module Dyndnsd
|
|
56
56
|
return false if not name.match(/^[a-zA-Z0-9_-]+\.$/)
|
57
57
|
true
|
58
58
|
end
|
59
|
-
|
59
|
+
|
60
60
|
def call(env)
|
61
61
|
return @responder.response_for_error(:method_forbidden) if env["REQUEST_METHOD"] != "GET"
|
62
62
|
return @responder.response_for_error(:not_found) if env["PATH_INFO"] != "/nic/update"
|
63
|
-
|
63
|
+
|
64
64
|
params = Rack::Utils.parse_query(env["QUERY_STRING"])
|
65
|
-
|
65
|
+
|
66
66
|
return @responder.response_for_error(:hostname_missing) if not params["hostname"]
|
67
|
-
|
67
|
+
|
68
68
|
hostnames = params["hostname"].split(',')
|
69
|
-
|
69
|
+
|
70
70
|
# Check if hostname match rules
|
71
71
|
hostnames.each do |hostname|
|
72
72
|
return @responder.response_for_error(:hostname_malformed) if not is_fqdn_valid?(hostname)
|
73
73
|
end
|
74
|
-
|
74
|
+
|
75
75
|
user = env["REMOTE_USER"]
|
76
|
-
|
76
|
+
|
77
77
|
hostnames.each do |hostname|
|
78
78
|
return @responder.response_for_error(:host_forbidden) if not @users[user]['hosts'].include? hostname
|
79
79
|
end
|
80
80
|
|
81
|
-
#
|
82
|
-
|
83
|
-
|
81
|
+
# fallback value, always present
|
82
|
+
myip = env["REMOTE_ADDR"]
|
83
|
+
|
84
|
+
# check whether X-Real-IP header has valid IPAddr
|
85
|
+
if env.has_key?("HTTP_X_REAL_IP")
|
86
|
+
begin
|
87
|
+
IPAddr.new(env["HTTP_X_REAL_IP"])
|
88
|
+
myip = env["HTTP_X_REAL_IP"]
|
89
|
+
rescue ArgumentError
|
90
|
+
end
|
84
91
|
end
|
85
|
-
|
86
|
-
#
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
92
|
+
|
93
|
+
# check whether myip parameter has valid IPAddr
|
94
|
+
if params.has_key?("myip")
|
95
|
+
begin
|
96
|
+
IPAddr.new(params["myip"])
|
97
|
+
myip = params["myip"]
|
98
|
+
rescue ArgumentError
|
99
|
+
end
|
91
100
|
end
|
92
|
-
|
93
|
-
myip = params["myip"]
|
94
|
-
|
101
|
+
|
95
102
|
Metriks.meter('requests.valid').mark
|
96
103
|
Dyndnsd.logger.info "Request to update #{hostnames} to #{myip} for user #{user}"
|
97
|
-
|
104
|
+
|
98
105
|
changes = []
|
99
106
|
hostnames.each do |hostname|
|
100
107
|
if (not @db['hosts'].include? hostname) or (@db['hosts'][hostname] != myip)
|
@@ -106,7 +113,7 @@ module Dyndnsd
|
|
106
113
|
Metriks.meter('requests.nochg').mark
|
107
114
|
end
|
108
115
|
end
|
109
|
-
|
116
|
+
|
110
117
|
if @db.changed?
|
111
118
|
@db['serial'] += 1
|
112
119
|
Dyndnsd.logger.info "Committing update ##{@db['serial']}"
|
@@ -114,7 +121,7 @@ module Dyndnsd
|
|
114
121
|
update
|
115
122
|
Metriks.meter('updates.committed').mark
|
116
123
|
end
|
117
|
-
|
124
|
+
|
118
125
|
@responder.response_for_changes(changes, myip)
|
119
126
|
end
|
120
127
|
|
@@ -130,23 +137,23 @@ module Dyndnsd
|
|
130
137
|
puts "Config file not found!"
|
131
138
|
exit 1
|
132
139
|
end
|
133
|
-
|
140
|
+
|
134
141
|
puts "DynDNSd version #{Dyndnsd::VERSION}"
|
135
142
|
puts "Using config file #{config_file}"
|
136
143
|
|
137
144
|
config = YAML::load(File.open(config_file, 'r') { |f| f.read })
|
138
|
-
|
145
|
+
|
139
146
|
if config['logfile']
|
140
147
|
Dyndnsd.logger = Logger.new(config['logfile'])
|
141
148
|
else
|
142
149
|
Dyndnsd.logger = Logger.new(STDOUT)
|
143
150
|
end
|
144
|
-
|
151
|
+
|
145
152
|
Dyndnsd.logger.progname = "dyndnsd"
|
146
153
|
Dyndnsd.logger.formatter = LogFormatter.new
|
147
154
|
|
148
155
|
Dyndnsd.logger.info "Starting..."
|
149
|
-
|
156
|
+
|
150
157
|
# drop privs (first change group than user)
|
151
158
|
Process::Sys.setgid(Etc.getgrnam(config['group']).gid) if config['group']
|
152
159
|
Process::Sys.setuid(Etc.getpwnam(config['user']).uid) if config['user']
|
@@ -174,7 +181,7 @@ module Dyndnsd
|
|
174
181
|
db = Database.new(config['db'])
|
175
182
|
updater = Updater::CommandWithBindZone.new(config['domain'], config['updater']['params']) if config['updater']['name'] == 'command_with_bind_zone'
|
176
183
|
responder = Responder::DynDNSStyle.new
|
177
|
-
|
184
|
+
|
178
185
|
# configure rack
|
179
186
|
app = Daemon.new(config, db, updater, responder)
|
180
187
|
app = Rack::Auth::Basic.new(app, "DynDNS") do |user,pass|
|
data/lib/dyndnsd/version.rb
CHANGED
data/spec/daemon_spec.rb
CHANGED
@@ -2,11 +2,11 @@ require 'spec_helper'
|
|
2
2
|
|
3
3
|
describe Dyndnsd::Daemon do
|
4
4
|
include Rack::Test::Methods
|
5
|
-
|
5
|
+
|
6
6
|
def app
|
7
7
|
Dyndnsd.logger = Logger.new(STDOUT)
|
8
8
|
Dyndnsd.logger.level = Logger::UNKNOWN
|
9
|
-
|
9
|
+
|
10
10
|
config = {
|
11
11
|
'domain' => 'example.org',
|
12
12
|
'users' => {
|
@@ -20,123 +20,158 @@ describe Dyndnsd::Daemon do
|
|
20
20
|
updater = Dyndnsd::Updater::Dummy.new
|
21
21
|
responder = Dyndnsd::Responder::DynDNSStyle.new
|
22
22
|
app = Dyndnsd::Daemon.new(config, db, updater, responder)
|
23
|
-
|
23
|
+
|
24
24
|
Rack::Auth::Basic.new(app, "DynDNS") do |user,pass|
|
25
25
|
(config['users'].has_key? user) and (config['users'][user]['password'] == pass)
|
26
26
|
end
|
27
27
|
end
|
28
|
-
|
28
|
+
|
29
29
|
it 'requires authentication' do
|
30
30
|
get '/'
|
31
|
-
last_response.status.
|
32
|
-
|
31
|
+
expect(last_response.status).to eq(401)
|
32
|
+
|
33
33
|
pending 'Need to find a way to add custom body on 401 responses'
|
34
|
-
last_response.
|
34
|
+
expect(last_response).not_to be_ok
|
35
|
+
expect(last_response.body).to eq('badauth')
|
35
36
|
end
|
36
|
-
|
37
|
+
|
37
38
|
it 'only supports GET requests' do
|
38
39
|
authorize 'test', 'secret'
|
39
40
|
post '/nic/update'
|
40
|
-
last_response.status.
|
41
|
+
expect(last_response.status).to eq(405)
|
41
42
|
end
|
42
|
-
|
43
|
+
|
43
44
|
it 'provides only the /nic/update URL' do
|
44
45
|
authorize 'test', 'secret'
|
45
46
|
get '/other/url'
|
46
|
-
last_response.status.
|
47
|
+
expect(last_response.status).to eq(404)
|
47
48
|
end
|
48
|
-
|
49
|
+
|
49
50
|
it 'requires the hostname query parameter' do
|
50
51
|
authorize 'test', 'secret'
|
51
52
|
get '/nic/update'
|
52
|
-
last_response.
|
53
|
-
last_response.body.
|
53
|
+
expect(last_response).to be_ok
|
54
|
+
expect(last_response.body).to eq('notfqdn')
|
54
55
|
end
|
55
56
|
|
56
57
|
it 'supports multiple hostnames in request' do
|
57
58
|
authorize 'test', 'secret'
|
59
|
+
|
58
60
|
get '/nic/update?hostname=foo.example.org,bar.example.org&myip=1.2.3.4'
|
59
|
-
last_response.
|
60
|
-
last_response.body.
|
61
|
+
expect(last_response).to be_ok
|
62
|
+
expect(last_response.body).to eq("good 1.2.3.4\ngood 1.2.3.4")
|
63
|
+
|
64
|
+
get '/nic/update?hostname=foo.example.org,bar.example.org&myip=2001:db8::1'
|
65
|
+
expect(last_response).to be_ok
|
66
|
+
expect(last_response.body).to eq("good 2001:db8::1\ngood 2001:db8::1")
|
61
67
|
end
|
62
68
|
|
63
69
|
it 'rejects request if one hostname is invalid' do
|
64
70
|
authorize 'test', 'secret'
|
65
|
-
|
71
|
+
|
66
72
|
get '/nic/update?hostname=test'
|
67
|
-
last_response.
|
68
|
-
last_response.body.
|
69
|
-
|
73
|
+
expect(last_response).to be_ok
|
74
|
+
expect(last_response.body).to eq('notfqdn')
|
75
|
+
|
70
76
|
get '/nic/update?hostname=test.example.com'
|
71
|
-
last_response.
|
72
|
-
last_response.body.
|
73
|
-
|
77
|
+
expect(last_response).to be_ok
|
78
|
+
expect(last_response.body).to eq('notfqdn')
|
79
|
+
|
74
80
|
get '/nic/update?hostname=test.example.org.me'
|
75
|
-
last_response.
|
76
|
-
last_response.body.
|
77
|
-
|
81
|
+
expect(last_response).to be_ok
|
82
|
+
expect(last_response.body).to eq('notfqdn')
|
83
|
+
|
78
84
|
get '/nic/update?hostname=foo.test.example.org'
|
79
|
-
last_response.
|
80
|
-
last_response.body.
|
81
|
-
|
85
|
+
expect(last_response).to be_ok
|
86
|
+
expect(last_response.body).to eq('notfqdn')
|
87
|
+
|
82
88
|
get '/nic/update?hostname=in%20valid.example.org'
|
83
|
-
last_response.
|
84
|
-
last_response.body.
|
85
|
-
|
89
|
+
expect(last_response).to be_ok
|
90
|
+
expect(last_response.body).to eq('notfqdn')
|
91
|
+
|
86
92
|
get '/nic/update?hostname=valid.example.org,in.valid.example.org'
|
87
|
-
last_response.
|
88
|
-
last_response.body.
|
93
|
+
expect(last_response).to be_ok
|
94
|
+
expect(last_response.body).to eq('notfqdn')
|
89
95
|
end
|
90
|
-
|
96
|
+
|
91
97
|
it 'rejects request if user does not own one hostname' do
|
92
98
|
authorize 'test', 'secret'
|
93
99
|
get '/nic/update?hostname=notmyhost.example.org'
|
94
|
-
last_response.
|
95
|
-
last_response.body.
|
96
|
-
|
100
|
+
expect(last_response).to be_ok
|
101
|
+
expect(last_response.body).to eq('nohost')
|
102
|
+
|
97
103
|
get '/nic/update?hostname=foo.example.org,notmyhost.example.org'
|
98
|
-
last_response.
|
99
|
-
last_response.body.
|
104
|
+
expect(last_response).to be_ok
|
105
|
+
expect(last_response.body).to eq('nohost')
|
100
106
|
end
|
101
|
-
|
102
|
-
it 'updates a host on
|
107
|
+
|
108
|
+
it 'updates a host on IP change' do
|
103
109
|
authorize 'test', 'secret'
|
104
|
-
|
110
|
+
|
105
111
|
get '/nic/update?hostname=foo.example.org&myip=1.2.3.4'
|
106
|
-
last_response.
|
107
|
-
|
112
|
+
expect(last_response).to be_ok
|
113
|
+
|
108
114
|
get '/nic/update?hostname=foo.example.org&myip=1.2.3.40'
|
109
|
-
last_response.
|
110
|
-
last_response.body.
|
115
|
+
expect(last_response).to be_ok
|
116
|
+
expect(last_response.body).to eq('good 1.2.3.40')
|
117
|
+
|
118
|
+
get '/nic/update?hostname=foo.example.org&myip=2001:db8::1'
|
119
|
+
expect(last_response).to be_ok
|
120
|
+
|
121
|
+
get '/nic/update?hostname=foo.example.org&myip=2001:db8::10'
|
122
|
+
expect(last_response).to be_ok
|
123
|
+
expect(last_response.body).to eq('good 2001:db8::10')
|
111
124
|
end
|
112
|
-
|
113
|
-
it 'returns
|
125
|
+
|
126
|
+
it 'returns IP no change' do
|
114
127
|
authorize 'test', 'secret'
|
115
|
-
|
128
|
+
|
116
129
|
get '/nic/update?hostname=foo.example.org&myip=1.2.3.4'
|
117
|
-
last_response.
|
118
|
-
|
130
|
+
expect(last_response).to be_ok
|
131
|
+
|
119
132
|
get '/nic/update?hostname=foo.example.org&myip=1.2.3.4'
|
120
|
-
last_response.
|
121
|
-
last_response.body.
|
133
|
+
expect(last_response).to be_ok
|
134
|
+
expect(last_response.body).to eq('nochg 1.2.3.4')
|
135
|
+
|
136
|
+
get '/nic/update?hostname=foo.example.org&myip=2001:db8::1'
|
137
|
+
expect(last_response).to be_ok
|
138
|
+
|
139
|
+
get '/nic/update?hostname=foo.example.org&myip=2001:db8::1'
|
140
|
+
expect(last_response).to be_ok
|
141
|
+
expect(last_response.body).to eq('nochg 2001:db8::1')
|
122
142
|
end
|
123
|
-
|
124
|
-
it 'outputs
|
143
|
+
|
144
|
+
it 'outputs IP status per hostname' do
|
125
145
|
authorize 'test', 'secret'
|
126
146
|
|
127
147
|
get '/nic/update?hostname=foo.example.org&myip=1.2.3.4'
|
128
|
-
last_response.
|
129
|
-
last_response.body.
|
130
|
-
|
148
|
+
expect(last_response).to be_ok
|
149
|
+
expect(last_response.body).to eq('good 1.2.3.4')
|
150
|
+
|
131
151
|
get '/nic/update?hostname=foo.example.org,bar.example.org&myip=1.2.3.4'
|
132
|
-
last_response.
|
133
|
-
last_response.body.
|
152
|
+
expect(last_response).to be_ok
|
153
|
+
expect(last_response.body).to eq("nochg 1.2.3.4\ngood 1.2.3.4")
|
154
|
+
|
155
|
+
get '/nic/update?hostname=foo.example.org&myip=2001:db8::1'
|
156
|
+
expect(last_response).to be_ok
|
157
|
+
expect(last_response.body).to eq('good 2001:db8::1')
|
158
|
+
|
159
|
+
get '/nic/update?hostname=foo.example.org,bar.example.org&myip=2001:db8::1'
|
160
|
+
expect(last_response).to be_ok
|
161
|
+
expect(last_response.body).to eq("nochg 2001:db8::1\ngood 2001:db8::1")
|
134
162
|
end
|
135
|
-
|
136
|
-
it 'uses clients remote
|
163
|
+
|
164
|
+
it 'uses clients remote IP address if myip not specified' do
|
137
165
|
authorize 'test', 'secret'
|
138
166
|
get '/nic/update?hostname=foo.example.org'
|
139
|
-
last_response.
|
140
|
-
last_response.body.
|
167
|
+
expect(last_response).to be_ok
|
168
|
+
expect(last_response.body).to eq('good 127.0.0.1')
|
169
|
+
end
|
170
|
+
|
171
|
+
it 'uses clients remote IP address from X-Real-IP header if behind proxy' do
|
172
|
+
authorize 'test', 'secret'
|
173
|
+
get '/nic/update?hostname=foo.example.org', '', 'HTTP_X_REAL_IP' => '10.0.0.1'
|
174
|
+
expect(last_response).to be_ok
|
175
|
+
expect(last_response.body).to eq('good 10.0.0.1')
|
141
176
|
end
|
142
177
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: dyndnsd
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.5.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Christian Nicolai
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-11-
|
11
|
+
date: 2016-11-30 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rack
|
@@ -28,16 +28,16 @@ dependencies:
|
|
28
28
|
name: json
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
|
-
- - "
|
31
|
+
- - "~>"
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version: '
|
33
|
+
version: '1.8'
|
34
34
|
type: :runtime
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
|
-
- - "
|
38
|
+
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version: '
|
40
|
+
version: '1.8'
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
42
|
name: metriks
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|