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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: ef4ecc3e27d9e5a2ffaeaed01b8536c01fe5de68
4
- data.tar.gz: 6fcaf18ad03cccc5ab1e508d00bc31ff3f4fcd4f
3
+ metadata.gz: 83264e341913c7b4dfc4e79402a60b3284a5bb41
4
+ data.tar.gz: 51af5408a701ca8829d3e73068c6bb198e3dc1b6
5
5
  SHA512:
6
- metadata.gz: fd3fd77b9cef5bd9f7d1c165f8e6386613deea05b02cfd911c04899396c28c3f95b809b7d82d253ae5f8d5ff14c5b6f7f4b26049e59a308660164e76f2cacc1a
7
- data.tar.gz: fa4587358033f6730f32f412ff663c0f21078e791c534931a4fce7e816326ad1037b94b4f31d4ba3c45eae20d67699aaf7f64017c12efa38fce3598416bced7d
6
+ metadata.gz: 810aca8e4360ec88e959f116460841638824174d87b841adc7396c3fa3226106ed673aa678a24e33554d24252e7c704300bdb59ccdb266661fe10723c7dff842
7
+ data.tar.gz: b056c351200e8ad50d120874ad77e466eb50bd27ee567722e5f9bf6bfe6c1481cf9c9f75de344d79c0855c102a45feb63566ddd7bda49581c5f72d4c338028ad
data/.travis.yml CHANGED
@@ -3,7 +3,6 @@ language: ruby
3
3
  rvm:
4
4
  - 2.0.0
5
5
  - 1.9.3
6
- - 1.8.7
7
6
 
8
7
  gemfile:
9
8
  - Gemfile
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 is aimed to implement a small [DynDNS-compliant](http://dyn.com/support/developers/api/) server in Ruby. 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.
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
- The 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.
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 *nix systems.
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
- # no myip?
82
- if not params["myip"]
83
- params["myip"] = env["REMOTE_ADDR"]
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
- # malformed myip?
87
- begin
88
- IPAddr.new(params["myip"])
89
- rescue ArgumentError
90
- params["myip"] = env["REMOTE_ADDR"]
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|
@@ -1,4 +1,4 @@
1
1
 
2
2
  module Dyndnsd
3
- VERSION = "1.4.0"
3
+ VERSION = "1.5.0"
4
4
  end
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.should == 401
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.should be_ok 'badauth'
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.should == 405
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.should == 404
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.should be_ok
53
- last_response.body.should == 'notfqdn'
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.should be_ok
60
- last_response.body.should == "good 1.2.3.4\ngood 1.2.3.4"
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.should be_ok
68
- last_response.body.should == 'notfqdn'
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.should be_ok
72
- last_response.body.should == 'notfqdn'
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.should be_ok
76
- last_response.body.should == 'notfqdn'
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.should be_ok
80
- last_response.body.should == 'notfqdn'
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.should be_ok
84
- last_response.body.should == 'notfqdn'
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.should be_ok
88
- last_response.body.should == 'notfqdn'
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.should be_ok
95
- last_response.body.should == 'nohost'
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.should be_ok
99
- last_response.body.should == 'nohost'
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 IPv4 change' do
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.should be_ok
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.should be_ok
110
- last_response.body.should == 'good 1.2.3.40'
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 IPv4 no change' do
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.should be_ok
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.should be_ok
121
- last_response.body.should == 'nochg 1.2.3.4'
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 IPv4 status per hostname' do
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.should be_ok
129
- last_response.body.should == 'good 1.2.3.4'
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.should be_ok
133
- last_response.body.should == "nochg 1.2.3.4\ngood 1.2.3.4"
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 IPv4 address if myip not specified' do
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.should be_ok
140
- last_response.body.should == 'good 127.0.0.1'
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.0
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-27 00:00:00.000000000 Z
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: '0'
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: '0'
40
+ version: '1.8'
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: metriks
43
43
  requirement: !ruby/object:Gem::Requirement