# frozen_string_literal: true
#
# Copyright (c) 2006-2022 Hal Brodigan (postmodern.mod3 at gmail.com)
#
# ronin-support is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# ronin-support is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with ronin-support. If not, see .
#
require 'ronin/support/network/exceptions'
require 'ronin/support/home'
require 'net/https'
require 'fileutils'
module Ronin
module Support
module Network
module TLD
#
# Represents the [Top-Level Domains list].
#
# [Top-Level Domains list]: https://www.icann.org/resources/pages/tlds-2012-02-25-en
#
# @api public
#
# @since 1.0.0
#
class List
include Enumerable
# File name of the TLD list.
FILE_NAME = 'tlds-alpha-by-domain.txt'
# The `https://data.iana.org/TLD/tlds-alpha-by-domain.txt`
URL = "https://data.iana.org/TLD/#{FILE_NAME}"
# The path to `~/.cache/ronin/ronin-support/tlds-alpha-by-domain.txt` list file.
PATH = File.join(Home::CACHE_DIR,'ronin','ronin-support',FILE_NAME)
# The path to the list file.
#
# @return [String]
attr_reader :path
# The list of all TLDs.
#
# @return [Array]
attr_reader :list
# The tree of all TLD TLDs.
#
# @return [Hash{String => Hash}]
attr_reader :tree
#
# Initializes the TLD list.
#
# @param [String] path
# The path to the list file.
#
# @api private
#
def initialize(path=PATH)
@path = path
@list = []
@tree = {}
end
#
# Determines whether the list file has been previously downloaded.
#
# @param [String] path
# An optional alternate path to the list file.
#
# @return [Boolean]
#
def self.downloaded?(path=PATH)
File.file?(path)
end
# One day in seconds.
ONE_DAY = 60 * 60 * 24
#
# Determines if the downloaded list file is older than one day.
#
# @param [String] path
# An optional alternate path to the list file.
#
# @return [Boolean]
#
def self.stale?(path=PATH)
!File.file?(path) || File.stat(path).mtime < (Time.now - ONE_DAY)
end
#
# Downloads the list file.
#
# @param [String] url
# An optional alternate URL to download the `ip2asn-combined.tsv.gz`
# file.
#
# @param [String] path
# An optional alternate path to the list file.
#
def self.download(url: URL, path: PATH)
uri = URI(url)
Net::HTTP.start(uri.host,uri.port, use_ssl: true) do |http|
request = Net::HTTP::Get.new(uri.path)
http.request(request) do |response|
FileUtils.mkdir_p(File.dirname(path))
File.open("#{path}.part",'wb') do |file|
response.read_body do |chunk|
file.write(chunk)
end
end
FileUtils.mv("#{path}.part",path)
end
end
end
#
# Optionally update the cached list file if it is older than one day.
#
# @param [String] url
# An optional alternate URL to download the `ip2asn-combined.tsv.gz`
# file.
#
# @param [String] path
# An optional alternate path to the list file.
#
def self.update(url: URL, path: PATH)
if !downloaded?(path)
download(url: url, path: path)
elsif stale?(path)
begin
download(url: url, path: path)
rescue
end
end
end
#
# Loads the TLD list from the given file.
#
# @param [String] path
# The path to the TLD list file.
#
# @return [List]
# The parsed TLD list file.
#
def self.load_file(path=PATH)
list = new(path)
File.open(path) do |file|
file.each_line(chomp: true) do |line|
next if line.start_with?('#')
list << line.downcase
end
end
return list
end
#
# Adds a TLD to the list.
#
# @param [String] tld
# The TLD String to add.
#
# @return [self]
#
# @api private
#
def <<(tld)
@list << tld
return self
end
#
# Enumerates over each suffix in the list.
#
# @yield [suffix]
# If a block is given, it will be passed each suffix in the list.
#
# @yieldparam [String] suffix
# A domain suffix in the list.
#
# @return [Enumerator]
# If no block is given, an Enumerator object will be returned.
#
def each(&block)
@list.each(&block)
end
#
# Splits a hostname into it's name and TLD components.
#
# @param [String] host_name
# The host name to split.
#
# @return [(String, String)]
# The host name's name and TLD components.
#
# @raise [InvalidHostname]
# The given hostname does not end with a valid TLD.
#
def split(host_name)
unless (index = host_name.rindex('.'))
raise(InvalidHostname,"hostname does not have a TLD: #{host_name.inspect}")
end
name = host_name[0...index]
tld = host_name[(index+1)..]
unless @list.include?(tld)
raise(InvalidHostname,"hostname does not have a valid TLD: #{host_name.inspect}")
end
return name, tld
end
#
# Creates a regular expression that can match every domain suffix in
# the list.
#
# @return [Regexp]
#
def to_regexp
regexp = Regexp.union(@list)
return /(?<=[^a-zA-Z0-9_-]|^)#{regexp}(?=[^\.a-z0-9-]|$)/
end
#
# Inspects the TLD list.
#
# @return [String]
# The inspected list object.
#
def inspect
"#<#{self.class}: #{@path}>"
end
end
end
end
end
end