lib/utility/middleware/restrict_hostnames.rb (54 lines of code) (raw):
#
# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
# or more contributor license agreements. Licensed under the Elastic License;
# you may not use this file except in compliance with the Elastic License.
#
# frozen_string_literal: true
require 'faraday/middleware'
require 'resolv'
require 'utility/errors'
require 'utility/logger'
module Utility
module Middleware
class RestrictHostnames < Faraday::Middleware
class AddressNotAllowed < Utility::ClientError; end
URL_PATTERN = /\Ahttp/
attr_reader :allowed_hosts, :allowed_ips
def initialize(app = nil, options = {})
super(app)
@allowed_hosts = options[:allowed_hosts]
@allowed_ips = ips_from_hosts(@allowed_hosts)
end
def call(env)
raise AddressNotAllowed.new("Address not allowed for #{env[:url]}") if denied?(env)
@app.call(env)
end
private
def ips_from_hosts(hosts)
hosts&.flat_map do |host|
if URL_PATTERN.match(host)
lookup_ips(Addressable::URI.parse(host).hostname)
elsif Resolv::IPv4::Regex.match(host) || Resolv::IPv6::Regex.match(host)
IPAddr.new(host)
else
lookup_ips(host)
end
end || []
end
def denied?(env)
requested_ips = lookup_ips(env[:url].hostname)
no_match = requested_ips.all? { |ip| !@allowed_ips.include?(ip) }
return false unless no_match
Utility::Logger.warn("Requested url #{env[:url]} with resolved ip addresses #{requested_ips} does not match " \
"allowed hosts #{@allowed_hosts} with resolved ip addresses #{@allowed_ips}. Retrying.")
@allowed_ips = ips_from_hosts(@allowed_hosts) # maybe the IP has changed for an allowed host. Re-do allowed_hosts DNS lookup
no_match = requested_ips.all? { |ip| !@allowed_ips.include?(ip) }
Utility::Logger.error("Requested url #{env[:url]} with resolved ip addresses #{requested_ips} does not match " \
"allowed hosts #{@allowed_hosts} with resolved ip addresses #{@allowed_ips}") if no_match
no_match
end
def lookup_ips(hostname)
addr_infos(hostname).map { |a| IPAddr.new(a.ip_address) }
end
def addr_infos(hostname)
Addrinfo.getaddrinfo(hostname, nil, :UNSPEC, :STREAM)
rescue SocketError
# In case of invalid hostname, return an empty list of addresses
[]
end
end
end
end