# frozen_string_literal: true
#
# ronin-db-activerecord - ActiveRecord backend for the Ronin Database.
#
# Copyright (c) 2022 Hal Brodigan (postmodern.mod3 at gmail.com)
#
# ronin-db-activerecord 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-db-activerecord 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-db-activerecord. If not, see .
#
require 'ronin/db/model'
require 'ronin/db/model/importable'
require 'ronin/db/model/last_scanned_at'
require 'active_record'
require 'uri/generic'
require 'uri/http'
require 'uri/https'
require 'uri/ftp'
require 'uri/query_params'
module Ronin
module DB
#
# Represents parsed URLs.
#
class URL < ActiveRecord::Base
include Model
include Model::Importable
include Model::LastScannedAt
# Mapping of URL Schemes and URI classes
SCHEMES = {
'https' => ::URI::HTTPS,
'http' => ::URI::HTTP,
'ftp' => ::URI::FTP
}
# @!attribute [rw] id
# The primary key of the URL.
#
# @return [Integer]
attribute :id, :integer
# @!attribute [rw] scheme
# The scheme of the URL.
#
# @return [URLScheme]
belongs_to :scheme, required: true,
class_name: 'URLScheme'
# @!attribute [rw] host_name
# The host name of the URL
#
# @return [HostName]
belongs_to :host_name, required: true
# @!attribute [rw] port
# The port of the URL.
#
# @return [Port, nil]
belongs_to :port, optional: true,
class_name: 'Port'
# @!attribute [rw] path
# The path of the URL.
#
# @return [String]
attribute :path, :string
# @!attribute [rw] query
# The query string part of the URL.
#
# @return [String, nil]
attribute :query, :string
# @!attribute [rw] fragment
# The fragment of the URL.
#
# @return [String, nil]
attribute :fragment, :string
# @!attribute [r] created_at
# Defines the created_at timestamp
#
# @return [Time]
attribute :created_at, :time
# @!attribute [rw] query_params
# The query params of the URL.
#
# @return [Array]
has_many :query_params, class_name: 'URLQueryParam',
dependent: :destroy
# @!attribute [rw] web_credentials
# Any credentials used with the URL.
#
# @return [Array]
has_many :web_credentials, dependent: :destroy
# @!attribute [rw] credentials
# The credentials that will work with this URL.
#
# @return [Array]
has_many :credentials, through: :web_credentials
#
# Searches for all URLs using HTTP.
#
# @return [Array]
# The matching URLs.
#
# @api public
#
def self.http
joins(:scheme).where(scheme: {name: 'http'})
end
#
# Searches for all URLs using HTTPS.
#
# @return [Array]
# The matching URLs.
#
# @api public
#
def self.https
joins(:scheme).where(scheme: {name: 'https'})
end
#
# Searches for URLs with specific host name(s).
#
# @param [String, Array] name
# The host name(s) to search for.
#
# @return [Array]
# The matching URLs.
#
# @api public
#
def self.with_host_name(name)
joins(:host_name).where(host_name: {name: name})
end
#
# Searches for URLs with the specific port number(s).
#
# @param [Integer, Array] number
# The port number(s) to search for.
#
# @return [Array]
# The matching URLs.
#
# @api public
#
def self.with_port_number(number)
joins(:port).where(port: {number: number})
end
#
# Searches for all URLs with the exact path.
#
# @param [String] path
# The path to search for.
#
# @return [Array]
# The URL with the matching path.
#
# @api public
#
def self.with_path(path)
where(path: path)
end
#
# Searches for all URLs with the exact fragment.
#
# @param [String] fragment
# The fragment to search for.
#
# @return [Array]
# The URL with the matching fragment.
#
# @api public
#
def self.with_fragment(fragment)
where(fragment: fragment)
end
#
# Searches for all URLs sharing a common sub-directory.
#
# @param [String] root_dir
# The sub-directory to search for.
#
# @return [Array]
# The URL with the common sub-directory.
#
# @api public
#
def self.with_directory(root_dir)
path_column = self.arel_table[:path]
where(path: root_dir).or(where(path_column.matches("#{root_dir}/%")))
end
#
# Searches for all URLs sharing a common base name.
#
# @param [String] basename
# The base name to search for.
#
# @return [Array]
# The URL with the common base name.
#
# @api public
#
def self.with_basename(basename)
path_column = self.arel_table[:path]
where(path_column.matches("%/#{basename}"))
end
#
# Searches for all URLs with a common file-extension.
#
# @param [String] ext
# The file extension to search for.
#
# @return [Array]
# The URLs with the common file-extension.
#
# @api public
#
def self.with_file_ext(ext)
path_column = self.arel_table[:path]
where(path_column.matches("%.#{sanitize_sql_like(ext)}"))
end
#
# Searches for URLs with the given query param name and value.
#
# @param [String, Array] name
# The query param name to search for.
#
# @param [String, Array] value
# The query param value to search for.
#
# @return [Array]
# The URLs with the given query param.
#
# @api public
#
def self.with_query_param(name,value)
joins(query_params: :name).where(
query_params: {
ronin_url_query_param_names: {name: name},
value: value
}
)
end
#
# Search for all URLs with a given query param name.
#
# @param [Array, String] name
# The query param name to search for.
#
# @return [Array]
# The URLs with the given query param name.
#
# @api public
#
def self.with_query_param_name(name)
joins(query_params: [:name]).where(
query_params: {
ronin_url_query_param_names: {name: name}
}
)
end
#
# Search for all URLs with a given query param value.
#
# @param [Array, String] value
# The query param value to search for.
#
# @return [Array]
# The URLs with the given query param value.
#
# @api public
#
def self.with_query_param_value(value)
joins(:query_params).where(query_params: {value: value})
end
#
# Searches for a URL.
#
# @param [URI::HTTP, String] url
# The URL to search for.
#
# @return [URL, nil]
# The matching URL.
#
# @api public
#
def self.lookup(url)
uri = URI(url)
# create the initial query
query = joins(:scheme, :host_name).where(
scheme: {name: uri.scheme},
host_name: {name: uri.host},
path: normalized_path(uri),
query: uri.query,
fragment: uri.fragment
)
if uri.port
# query the port
query = query.joins(:port).where(port: {number: uri.port})
end
return query.first
end
#
# Creates a new URL.
#
# @param [String, URI::HTTP] uri
# The URI to create the URL from.
#
# @return [URL]
# The new URL.
#
# @api public
#
def self.import(uri)
uri = URI(uri)
# find or create the URL scheme, host_name and port
scheme = URLScheme.find_or_create_by(name: uri.scheme)
host_name = HostName.find_or_create_by(name: uri.host)
port = if uri.port
Port.find_or_create_by(
protocol: :tcp,
number: uri.port
)
end
path = normalized_path(uri)
query = uri.query
fragment = uri.fragment
query_params = []
if uri.respond_to?(:query_params)
# find or create the URL query params
uri.query_params.each do |name,value|
query_params << URLQueryParam.new(
name: URLQueryParamName.find_or_create_by(name: name),
value: value
)
end
end
# find or create the URL
return find_or_create_by(
scheme: scheme,
host_name: host_name,
port: port,
path: path,
query: query,
fragment: fragment,
query_params: query_params
)
end
#
# The host name of the URL.
#
# @return [String]
# The address of host name.
#
# @api public
#
def host
self.host_name.name
end
#
# The port number used by the URL.
#
# @return [Integer, nil]
# The port number.
#
# @api public
#
def port_number
self.port.number if self.port
end
#
# Builds a URI object from the URL.
#
# @return [URI::HTTP, URI::HTTPS]
# The URI object created from the URL attributes.
#
# @api public
#
def to_uri
# map the URL scheme to a URI class
url_class = SCHEMES.fetch(self.scheme.name,::URI::Generic)
scheme = if self.scheme
self.scheme.name
end
host = if self.host_name
self.host_name.name
end
port = if self.port
self.port.number
end
# build the URI
return url_class.build(
scheme: scheme,
host: host,
port: port,
path: self.path,
query: self.query,
fragment: self.fragment
)
end
#
# Converts the URL to a String.
#
# @return [String]
# The string form of the URL.
#
# @api public
#
def to_s
self.to_uri.to_s
end
protected
#
# Normalizes the path of a URI.
#
# @param [URI] uri
# The URI containing the path.
#
# @return [String, nil]
# The normalized path.
#
# @api private
#
def self.normalized_path(uri)
case uri
when ::URI::HTTP
# map empty HTTP paths to '/'
unless uri.path.empty? then uri.path
else '/'
end
else
uri.path
end
end
end
end
end
require 'ronin/db/host_name'
require 'ronin/db/port'
require 'ronin/db/url_scheme'
require 'ronin/db/url_query_param_name'
require 'ronin/db/url_query_param'
require 'ronin/db/web_credential'