class DNSUpdater::Updaters::HTTP
DNS updater over HTTP
Constants
- AUTH_NAME
Name of authentication - our own custom
- AUTH_VALID_SECONDS
Time how long authentication key is valid for
- DEFAULT_SETTINGS
Public Class Methods
buildAuthHMAC(secret, method, path, query = '', authValidTime = nil)
click to toggle source
Build authentication HMAC, taking into parameters and time If authValidTime is passed it will be decreased for use of retry
# File lib/dnsupdater/updaters/http.rb, line 77 def self.buildAuthHMAC(secret, method, path, query = '', authValidTime = nil) if authValidTime authValidTime -= 1 else # Will be valid only for limited time authValidTime = Time.now.to_i / AUTH_VALID_SECONDS end data = buildAuthString(authValidTime, method, path, query) [OpenSSL::HMAC.digest('SHA256', secret, data), authValidTime] end
buildAuthString(*params)
click to toggle source
# File lib/dnsupdater/updaters/http.rb, line 89 def self.buildAuthString(*params) params.join(':') end
formatResponse(code, message, extraHeaders = {})
click to toggle source
# File lib/dnsupdater/updaters/http.rb, line 60 def self.formatResponse(code, message, extraHeaders = {}) headers = { 'Content-Type' => 'application/json; charset=UTF-8' } headers.merge!(extraHeaders) data = { success: code == 200, message: message } [code, headers, [JSON.generate(data)]] end
getHostPort(config)
click to toggle source
@see Updater.getHostPort
# File lib/dnsupdater/updaters/http.rb, line 68 def self.getHostPort(config) [ config['HTTP']['Host'], config['HTTP']['Port'] ] end
getParams(target)
click to toggle source
# File lib/dnsupdater/updaters/http.rb, line 53 def self.getParams(target) params = DNSUpdater.buildParams DNSUpdater.fillPathParams(target, params) params end
Public Instance Methods
call(env)
click to toggle source
# File lib/dnsupdater/updaters/http.rb, line 38 def call(env) @ENV = env path = @ENV['PATH_INFO'] if isAuthenticated(@ENV['HTTP_AUTHORIZATION'], @ENV['REQUEST_METHOD'], path, @ENV['QUERY_STRING']) handleRequest(@ENV['REQUEST_METHOD'], path, CGI.parse(@ENV['QUERY_STRING'])) else authName = AUTH_NAME authName = 'Basic' if isDynDNS(path, CGI.parse(@ENV['QUERY_STRING'])) self.class.formatResponse(401, 'Unauthorized!', 'WWW-Authenticate' => authName) end rescue Error => e self.class.formatResponse(400, e.message) end
update(params)
click to toggle source
@see Updater#update
# File lib/dnsupdater/updaters/http.rb, line 27 def update(params) @ENV = nil fillParams(params) path = [nil, params[:Domain], params[:IPs].join(',')].join(Addressable::URI::SLASH) uri = Addressable::URI.new(scheme: params[:Protocol].to_s, host: params[:Server], port: params[:Port], path: path).normalize request = Net::HTTP::Post.new(uri.path) addAuthHeader(request, uri.path) sendRequest(uri, request) end
Private Instance Methods
addAuthHeader(request, path)
click to toggle source
# File lib/dnsupdater/updaters/http.rb, line 108 def addAuthHeader(request, path) secret = @Config['HTTP']['SharedSecret'].to_s return if secret.empty? hmac, = self.class.buildAuthHMAC(secret, 'POST', path) request['Authorization'] = AUTH_NAME + ' ' + Base64.urlsafe_encode64(hmac, padding: false) end
constantTimeEqual?(a, b)
click to toggle source
# File lib/dnsupdater/updaters/http.rb, line 164 def constantTimeEqual?(a, b) return false if a.bytesize != b.bytesize # OpenSSL.memcmp?(a, b) # Not released yet, should be in OpenSSL 2.2 Rack::Utils.secure_compare(a, b) end
fillParams(params)
click to toggle source
# File lib/dnsupdater/updaters/http.rb, line 116 def fillParams(params) httpConfig = @Config['HTTP'] params[:Server] = httpConfig['Host'] unless params[:Server] params[:Port] = httpConfig['Port'] unless params[:Port] params[:Protocol] = :http unless params.key?(:Protocol) targetProtocol = @Config.getTargetProtocol('HTTP') raise Error, 'Unsupported!' if %i[http https].include?(targetProtocol) params[:TargetParams] = {} params[:TargetParams][:Protocol] = targetProtocol params[:TargetParams][:Domain] = params[:Domain] end
getDynDNSUri(query)
click to toggle source
# File lib/dnsupdater/updaters/http.rb, line 135 def getDynDNSUri(query) '/' + query['hostname'].to_a.first.to_s + '/' + query['myip'].to_a.first.to_s end
getUri(method, path)
click to toggle source
# File lib/dnsupdater/updaters/http.rb, line 139 def getUri(method, path) raise Error, 'Invalid parameters!' unless method == 'POST' uri = path.dup uri.force_encoding('UTF-8') uri end
handleRequest(method, path, query)
click to toggle source
# File lib/dnsupdater/updaters/http.rb, line 151 def handleRequest(method, path, query) uri = isDynDNS(path, query) ? getDynDNSUri(query) : getUri(method, path) params = self.class.getParams(uri) fillParams(params) params[:TargetParams][:IPs] = getIPs(params[:IPs]) DNSUpdater.update(params[:TargetParams][:Protocol], params[:TargetParams], @Config) self.class.formatResponse(200, 'Updated!') end
isAuthenticated(authorizationHeader, method, path, query)
click to toggle source
# File lib/dnsupdater/updaters/http.rb, line 213 def isAuthenticated(authorizationHeader, method, path, query) if isDynDNS(path, CGI.parse(query)) isDynDNSAuthenticated(authorizationHeader) else isDNSUpdateAuthenticated(authorizationHeader, method, path, query) end end
isDNSUpdateAuthenticated(authorizationHeader, method, path, query)
click to toggle source
# File lib/dnsupdater/updaters/http.rb, line 186 def isDNSUpdateAuthenticated(authorizationHeader, method, path, query) secret = @Config['HTTP']['SharedSecret'].to_s authHeaderParts = authorizationHeader.to_s.split return false if authHeaderParts.length != 2 || authHeaderParts.first != AUTH_NAME || secret.empty? authHMAC = '' begin authHMAC = Base64.urlsafe_decode64(authHeaderParts.last) rescue ArgumentError authHMAC = '' end hmac, authValidTime = self.class.buildAuthHMAC(secret, method, path, query) return false if authHMAC.bytesize != hmac.bytesize # Compare in constant time to be timing safe result = constantTimeEqual?(authHMAC, hmac) unless result # In case of fail, retry again for previous time, see buildAuthHMAC hmac, = self.class.buildAuthHMAC(secret, method, path, query, authValidTime) result = constantTimeEqual?(authHMAC, hmac) end result end
isDynDNS(path, query)
click to toggle source
# File lib/dnsupdater/updaters/http.rb, line 147 def isDynDNS(path, query) path[-7..] == '/update' || query.key?('hostname') || query.key?('myip') end
isDynDNSAuthenticated(authorizationHeader)
click to toggle source
# File lib/dnsupdater/updaters/http.rb, line 171 def isDynDNSAuthenticated(authorizationHeader) secret = @Config['HTTP']['SharedSecret'].to_s authHeaderParts = authorizationHeader.to_s.split return false if authHeaderParts.length != 2 || authHeaderParts.first != 'Basic' || secret.empty? user, password = Base64.decode64(authHeaderParts.last).split(':') begin password = Base64.urlsafe_decode64(password.to_s) rescue ArgumentError password = nil end constantTimeEqual?(user.to_s, AUTH_NAME) & constantTimeEqual?(password.to_s, OpenSSL::HMAC.digest('SHA256', secret, AUTH_NAME)) end
resolveClient()
click to toggle source
# File lib/dnsupdater/updaters/http.rb, line 131 def resolveClient @ENV ? [@ENV['REMOTE_ADDR']] : nil end
sendRequest(uri, request)
click to toggle source
# File lib/dnsupdater/updaters/http.rb, line 95 def sendRequest(uri, request) Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http| response = http.request(request) result = { 'success' => false, 'message' => response.body } result = JSON.parse(result['message']).to_hash if result['message'].to_s[0] == '{' raise Error, self.class.name + ": [#{response.code} #{response.message}] " + result['message'].to_s if response.code.to_i != 200 || !result['success'] end rescue IOError, SocketError, SystemCallError, OpenSSL::OpenSSLError => e raise Error, self.class.name + ': ' + e.message rescue Interrupt raise Error, "\nCancelled!" end