mirror of
https://github.com/cmur2/dyndnsd.git
synced 2024-12-21 14:54:22 +01:00
responder: refactor into Rack middleware and improve API conformance
This commit is contained in:
parent
3f56070ed5
commit
f1b58f5167
@ -32,12 +32,11 @@ module Dyndnsd
|
|||||||
end
|
end
|
||||||
|
|
||||||
class Daemon
|
class Daemon
|
||||||
def initialize(config, db, updater, responder)
|
def initialize(config, db, updater)
|
||||||
@users = config['users']
|
@users = config['users']
|
||||||
@domain = config['domain']
|
@domain = config['domain']
|
||||||
@db = db
|
@db = db
|
||||||
@updater = updater
|
@updater = updater
|
||||||
@responder = responder
|
|
||||||
|
|
||||||
@db.load
|
@db.load
|
||||||
@db['serial'] ||= 1
|
@db['serial'] ||= 1
|
||||||
@ -58,31 +57,31 @@ module Dyndnsd
|
|||||||
end
|
end
|
||||||
|
|
||||||
def call(env)
|
def call(env)
|
||||||
return @responder.response_for_error(:method_forbidden) if env["REQUEST_METHOD"] != "GET"
|
return [422, {'X-DynDNS-Response' => 'method_forbidden'}, []] if env["REQUEST_METHOD"] != "GET"
|
||||||
return @responder.response_for_error(:not_found) if env["PATH_INFO"] != "/nic/update"
|
return [422, {'X-DynDNS-Response' => 'not_found'}, []] if env["PATH_INFO"] != "/nic/update"
|
||||||
|
|
||||||
params = Rack::Utils.parse_query(env["QUERY_STRING"])
|
params = Rack::Utils.parse_query(env["QUERY_STRING"])
|
||||||
|
|
||||||
return @responder.response_for_error(:hostname_missing) if not params["hostname"]
|
return [422, {'X-DynDNS-Response' => 'hostname_missing'}, []] if not params["hostname"]
|
||||||
|
|
||||||
hostnames = params["hostname"].split(',')
|
hostnames = params["hostname"].split(',')
|
||||||
|
|
||||||
# Check if hostname match rules
|
# Check if hostname match rules
|
||||||
hostnames.each do |hostname|
|
hostnames.each do |hostname|
|
||||||
return @responder.response_for_error(:hostname_malformed) if not is_fqdn_valid?(hostname)
|
return [422, {'X-DynDNS-Response' => 'hostname_malformed'}, []] if not is_fqdn_valid?(hostname)
|
||||||
end
|
end
|
||||||
|
|
||||||
user = env["REMOTE_USER"]
|
user = env["REMOTE_USER"]
|
||||||
|
|
||||||
hostnames.each do |hostname|
|
hostnames.each do |hostname|
|
||||||
return @responder.response_for_error(:host_forbidden) if not @users[user]['hosts'].include? hostname
|
return [422, {'X-DynDNS-Response' => 'host_forbidden'}, []] if not @users[user]['hosts'].include? hostname
|
||||||
end
|
end
|
||||||
|
|
||||||
myip = nil
|
myip = nil
|
||||||
|
|
||||||
if params.has_key?("myip6")
|
if params.has_key?("myip6")
|
||||||
# require presence of myip parameter as valid IPAddr (v4) and valid myip6
|
# require presence of myip parameter as valid IPAddr (v4) and valid myip6
|
||||||
return @responder.response_for_error(:host_forbidden) if not params["myip"]
|
return [422, {'X-DynDNS-Response' => 'host_forbidden'}, []] if not params["myip"]
|
||||||
begin
|
begin
|
||||||
IPAddr.new(params["myip"], Socket::AF_INET)
|
IPAddr.new(params["myip"], Socket::AF_INET)
|
||||||
IPAddr.new(params["myip6"], Socket::AF_INET6)
|
IPAddr.new(params["myip6"], Socket::AF_INET6)
|
||||||
@ -90,7 +89,7 @@ module Dyndnsd
|
|||||||
# myip will be an array
|
# myip will be an array
|
||||||
myip = [params["myip"], params["myip6"]]
|
myip = [params["myip"], params["myip6"]]
|
||||||
rescue ArgumentError
|
rescue ArgumentError
|
||||||
return @responder.response_for_error(:host_forbidden)
|
return [422, {'X-DynDNS-Response' => 'host_forbidden'}, []]
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
# fallback value, always present
|
# fallback value, always present
|
||||||
@ -138,7 +137,7 @@ module Dyndnsd
|
|||||||
Metriks.meter('updates.committed').mark
|
Metriks.meter('updates.committed').mark
|
||||||
end
|
end
|
||||||
|
|
||||||
@responder.response_for_changes(changes, myip)
|
[200, {'X-DynDNS-Response' => 'success'}, [changes, myip]]
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.run!
|
def self.run!
|
||||||
@ -196,10 +195,9 @@ module Dyndnsd
|
|||||||
# configure daemon
|
# configure daemon
|
||||||
db = Database.new(config['db'])
|
db = Database.new(config['db'])
|
||||||
updater = Updater::CommandWithBindZone.new(config['domain'], config['updater']['params']) if config['updater']['name'] == 'command_with_bind_zone'
|
updater = Updater::CommandWithBindZone.new(config['domain'], config['updater']['params']) if config['updater']['name'] == 'command_with_bind_zone'
|
||||||
responder = Responder::DynDNSStyle.new
|
|
||||||
|
|
||||||
# configure rack
|
# configure rack
|
||||||
app = Daemon.new(config, db, updater, responder)
|
app = Daemon.new(config, db, updater)
|
||||||
app = Rack::Auth::Basic.new(app, "DynDNS") do |user,pass|
|
app = Rack::Auth::Basic.new(app, "DynDNS") do |user,pass|
|
||||||
allow = ((config['users'].has_key? user) and (config['users'][user]['password'] == pass))
|
allow = ((config['users'].has_key? user) and (config['users'][user]['password'] == pass))
|
||||||
if not allow
|
if not allow
|
||||||
@ -209,6 +207,12 @@ module Dyndnsd
|
|||||||
allow
|
allow
|
||||||
end
|
end
|
||||||
|
|
||||||
|
if config['responder'] == 'RestStyle'
|
||||||
|
app = Responder::RestStyle.new(app)
|
||||||
|
else
|
||||||
|
app = Responder::DynDNSStyle.new(app)
|
||||||
|
end
|
||||||
|
|
||||||
Signal.trap('INT') do
|
Signal.trap('INT') do
|
||||||
Dyndnsd.logger.info "Quitting..."
|
Dyndnsd.logger.info "Quitting..."
|
||||||
Rack::Handler::WEBrick.shutdown
|
Rack::Handler::WEBrick.shutdown
|
||||||
|
@ -2,19 +2,53 @@
|
|||||||
module Dyndnsd
|
module Dyndnsd
|
||||||
module Responder
|
module Responder
|
||||||
class DynDNSStyle
|
class DynDNSStyle
|
||||||
def response_for_error(state)
|
def initialize(app)
|
||||||
# general http errors
|
@app = app
|
||||||
return [405, {"Content-Type" => "text/plain"}, ["Method Not Allowed"]] if state == :method_forbidden
|
|
||||||
return [404, {"Content-Type" => "text/plain"}, ["Not Found"]] if state == :not_found
|
|
||||||
# specific errors
|
|
||||||
return [200, {"Content-Type" => "text/plain"}, ["notfqdn"]] if state == :hostname_missing
|
|
||||||
return [200, {"Content-Type" => "text/plain"}, ["nohost"]] if state == :host_forbidden
|
|
||||||
return [200, {"Content-Type" => "text/plain"}, ["notfqdn"]] if state == :hostname_malformed
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def response_for_changes(states, ip)
|
def call(env)
|
||||||
body = states.map { |state| "#{state} #{ip.is_a?(Array) ? ip.join(' ') : ip}" }.join("\n")
|
@app.call(env).tap do |status_code, headers, body|
|
||||||
return [200, {"Content-Type" => "text/plain"}, [body]]
|
if headers.has_key?("X-DynDNS-Response")
|
||||||
|
return decorate_dyndnsd_response(status_code, headers, body)
|
||||||
|
else
|
||||||
|
return decorate_other_response(status_code, headers, body)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def decorate_dyndnsd_response(status_code, headers, body)
|
||||||
|
if status_code == 200
|
||||||
|
[200, {"Content-Type" => "text/plain"}, [get_success_body(body[0], body[1])]]
|
||||||
|
elsif status_code == 422
|
||||||
|
get_error_response_map[headers["X-DynDNS-Response"]]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def decorate_other_response(status_code, headers, body)
|
||||||
|
if status_code == 400
|
||||||
|
[status_code, headers, "Bad Request"]
|
||||||
|
elsif status_code == 401
|
||||||
|
[status_code, headers, "badauth"]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_success_body(states, ip)
|
||||||
|
ips = ip.is_a?(Array) ? ip.join(' ') : ip
|
||||||
|
states.map { |state| "#{state} #{ips}" }.join("\n")
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_error_response_map
|
||||||
|
{
|
||||||
|
# general http errors
|
||||||
|
'method_forbidden' => [405, {"Content-Type" => "text/plain"}, ["Method Not Allowed"]],
|
||||||
|
'not_found' => [404, {"Content-Type" => "text/plain"}, ["Not Found"]],
|
||||||
|
# specific errors
|
||||||
|
'hostname_missing' => [200, {"Content-Type" => "text/plain"}, ["notfqdn"]],
|
||||||
|
'hostname_malformed' => [200, {"Content-Type" => "text/plain"}, ["notfqdn"]],
|
||||||
|
'host_forbidden' => [200, {"Content-Type" => "text/plain"}, ["nohost"]]
|
||||||
|
}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -2,19 +2,54 @@
|
|||||||
module Dyndnsd
|
module Dyndnsd
|
||||||
module Responder
|
module Responder
|
||||||
class RestStyle
|
class RestStyle
|
||||||
def response_for_error(state)
|
def initialize(app)
|
||||||
# general http errors
|
@app = app
|
||||||
return [405, {"Content-Type" => "text/plain"}, ["Method Not Allowed"]] if state == :method_forbidden
|
|
||||||
return [404, {"Content-Type" => "text/plain"}, ["Not Found"]] if state == :not_found
|
|
||||||
# specific errors
|
|
||||||
return [422, {"Content-Type" => "text/plain"}, ["Hostname missing"]] if state == :hostname_missing
|
|
||||||
return [403, {"Content-Type" => "text/plain"}, ["Forbidden"]] if state == :host_forbidden
|
|
||||||
return [422, {"Content-Type" => "text/plain"}, ["Hostname malformed"]] if state == :hostname_malformed
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def response_for_changes(states, ip)
|
def call(env)
|
||||||
body = states.map { |state| state == :good ? "Changed to #{ip.is_a?(Array) ? ip.join(' ') : ip}" : "No change needed for #{ip.is_a?(Array) ? ip.join(' ') : ip}" }.join("\n")
|
@app.call(env).tap do |status_code, headers, body|
|
||||||
return [200, {"Content-Type" => "text/plain"}, [body]]
|
if headers.has_key?("X-DynDNS-Response")
|
||||||
|
return decorate_dyndnsd_response(status_code, headers, body)
|
||||||
|
else
|
||||||
|
return decorate_other_response(status_code, headers, body)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def decorate_dyndnsd_response(status_code, headers, body)
|
||||||
|
if status_code == 200
|
||||||
|
[200, {"Content-Type" => "text/plain"}, [get_success_body(body[0], body[1])]]
|
||||||
|
elsif status_code == 422
|
||||||
|
get_error_response_map[headers["X-DynDNS-Response"]]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def decorate_other_response(status_code, headers, body)
|
||||||
|
if status_code == 400
|
||||||
|
[status_code, headers, "Bad Request"]
|
||||||
|
elsif status_code == 401
|
||||||
|
[status_code, headers, "Unauthorized"]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
def get_success_response(states, ip)
|
||||||
|
ips = ip.is_a?(Array) ? ip.join(' ') : ip
|
||||||
|
states.map { |state| state == :good ? "Changed to #{ips}" : "No change needed for #{ips}" }.join("\n")
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_error_response_map
|
||||||
|
{
|
||||||
|
# general http errors
|
||||||
|
'method_forbidden' => [405, {"Content-Type" => "text/plain"}, ["Method Not Allowed"]],
|
||||||
|
'not_found' => [404, {"Content-Type" => "text/plain"}, ["Not Found"]],
|
||||||
|
# specific errors
|
||||||
|
'hostname_missing' => [422, {"Content-Type" => "text/plain"}, ["Hostname missing"]],
|
||||||
|
'hostname_malformed' => [422, {"Content-Type" => "text/plain"}, ["Hostname malformed"]],
|
||||||
|
'host_forbidden' => [403, {"Content-Type" => "text/plain"}, ["Forbidden"]]
|
||||||
|
}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -18,20 +18,18 @@ describe Dyndnsd::Daemon do
|
|||||||
}
|
}
|
||||||
db = Dyndnsd::DummyDatabase.new({})
|
db = Dyndnsd::DummyDatabase.new({})
|
||||||
updater = Dyndnsd::Updater::Dummy.new
|
updater = Dyndnsd::Updater::Dummy.new
|
||||||
responder = Dyndnsd::Responder::DynDNSStyle.new
|
app = Dyndnsd::Daemon.new(config, db, updater)
|
||||||
app = Dyndnsd::Daemon.new(config, db, updater, responder)
|
|
||||||
|
|
||||||
Rack::Auth::Basic.new(app, "DynDNS") do |user,pass|
|
app = Rack::Auth::Basic.new(app, "DynDNS") do |user,pass|
|
||||||
(config['users'].has_key? user) and (config['users'][user]['password'] == pass)
|
(config['users'].has_key? user) and (config['users'][user]['password'] == pass)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
app = Dyndnsd::Responder::DynDNSStyle.new(app)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'requires authentication' do
|
it 'requires authentication' do
|
||||||
get '/'
|
get '/'
|
||||||
expect(last_response.status).to eq(401)
|
expect(last_response.status).to eq(401)
|
||||||
|
|
||||||
pending 'Need to find a way to add custom body on 401 responses'
|
|
||||||
expect(last_response).not_to be_ok
|
|
||||||
expect(last_response.body).to eq('badauth')
|
expect(last_response.body).to eq('badauth')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user