From f1b58f516761ff9fde1f3b35c1bcecf6258a995d Mon Sep 17 00:00:00 2001 From: cn Date: Thu, 1 Feb 2018 20:26:48 +0100 Subject: [PATCH] responder: refactor into Rack middleware and improve API conformance --- lib/dyndnsd.rb | 28 +++++++------ lib/dyndnsd/responder/dyndns_style.rb | 56 ++++++++++++++++++++------ lib/dyndnsd/responder/rest_style.rb | 57 +++++++++++++++++++++------ spec/daemon_spec.rb | 10 ++--- 4 files changed, 111 insertions(+), 40 deletions(-) diff --git a/lib/dyndnsd.rb b/lib/dyndnsd.rb index 3705469..f5ccaee 100644 --- a/lib/dyndnsd.rb +++ b/lib/dyndnsd.rb @@ -32,12 +32,11 @@ module Dyndnsd end class Daemon - def initialize(config, db, updater, responder) + def initialize(config, db, updater) @users = config['users'] @domain = config['domain'] @db = db @updater = updater - @responder = responder @db.load @db['serial'] ||= 1 @@ -58,31 +57,31 @@ module Dyndnsd end def call(env) - return @responder.response_for_error(: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' => 'method_forbidden'}, []] if env["REQUEST_METHOD"] != "GET" + return [422, {'X-DynDNS-Response' => 'not_found'}, []] if env["PATH_INFO"] != "/nic/update" 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(',') # Check if hostname match rules 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 user = env["REMOTE_USER"] 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 myip = nil if params.has_key?("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 IPAddr.new(params["myip"], Socket::AF_INET) IPAddr.new(params["myip6"], Socket::AF_INET6) @@ -90,7 +89,7 @@ module Dyndnsd # myip will be an array myip = [params["myip"], params["myip6"]] rescue ArgumentError - return @responder.response_for_error(:host_forbidden) + return [422, {'X-DynDNS-Response' => 'host_forbidden'}, []] end else # fallback value, always present @@ -138,7 +137,7 @@ module Dyndnsd Metriks.meter('updates.committed').mark end - @responder.response_for_changes(changes, myip) + [200, {'X-DynDNS-Response' => 'success'}, [changes, myip]] end def self.run! @@ -196,10 +195,9 @@ module Dyndnsd # configure daemon db = Database.new(config['db']) updater = Updater::CommandWithBindZone.new(config['domain'], config['updater']['params']) if config['updater']['name'] == 'command_with_bind_zone' - responder = Responder::DynDNSStyle.new # 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| allow = ((config['users'].has_key? user) and (config['users'][user]['password'] == pass)) if not allow @@ -209,6 +207,12 @@ module Dyndnsd allow end + if config['responder'] == 'RestStyle' + app = Responder::RestStyle.new(app) + else + app = Responder::DynDNSStyle.new(app) + end + Signal.trap('INT') do Dyndnsd.logger.info "Quitting..." Rack::Handler::WEBrick.shutdown diff --git a/lib/dyndnsd/responder/dyndns_style.rb b/lib/dyndnsd/responder/dyndns_style.rb index 106f75a..8c33210 100644 --- a/lib/dyndnsd/responder/dyndns_style.rb +++ b/lib/dyndnsd/responder/dyndns_style.rb @@ -2,19 +2,53 @@ module Dyndnsd module Responder class DynDNSStyle - def response_for_error(state) - # general http errors - 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 + def initialize(app) + @app = app end - def response_for_changes(states, ip) - body = states.map { |state| "#{state} #{ip.is_a?(Array) ? ip.join(' ') : ip}" }.join("\n") - return [200, {"Content-Type" => "text/plain"}, [body]] + def call(env) + @app.call(env).tap do |status_code, headers, 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 diff --git a/lib/dyndnsd/responder/rest_style.rb b/lib/dyndnsd/responder/rest_style.rb index 681e5e0..5178e9e 100644 --- a/lib/dyndnsd/responder/rest_style.rb +++ b/lib/dyndnsd/responder/rest_style.rb @@ -2,19 +2,54 @@ module Dyndnsd module Responder class RestStyle - def response_for_error(state) - # general http errors - 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 + def initialize(app) + @app = app end - def response_for_changes(states, ip) - 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") - return [200, {"Content-Type" => "text/plain"}, [body]] + def call(env) + @app.call(env).tap do |status_code, headers, 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 diff --git a/spec/daemon_spec.rb b/spec/daemon_spec.rb index ba79ff5..e72a53b 100644 --- a/spec/daemon_spec.rb +++ b/spec/daemon_spec.rb @@ -18,20 +18,18 @@ describe Dyndnsd::Daemon do } db = Dyndnsd::DummyDatabase.new({}) updater = Dyndnsd::Updater::Dummy.new - responder = Dyndnsd::Responder::DynDNSStyle.new - app = Dyndnsd::Daemon.new(config, db, updater, responder) + app = Dyndnsd::Daemon.new(config, db, updater) - 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) end + + app = Dyndnsd::Responder::DynDNSStyle.new(app) end it 'requires authentication' do get '/' 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') end