# Copyright (c) 2023 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details. # frozen_string_literal: true require 'contrast/framework/base_support' require 'contrast/framework/rack/patch/support' require 'contrast/utils/duck_utils' module Contrast module Framework module Rack # Used when Rack is present to define framework specific behavior. For # now, the only part of this implemented is the Patch Support. module Support RACK_REQUEST_PATH = 'REQUEST_PATH' RACK_SERVER_NAME = 'SERVER_NAME' extend Contrast::Framework::BaseSupport extend Contrast::Framework::Rack::Patch::Support class << self def detection_class 'Rack' end # @return [String] the Rack version def version ::Rack.version rescue StandardError '' end # @return [String] the Rack application name def application_name 'Rack Application' end def application_root Dir.pwd end # @return [String] the server type def server_type 'Rack' end # Find all the predefined routes for this application # # Extracting the Rack application routes is not trivial. Routes are evaluated dynamically # when a request comes in, so they are not loaded before and stored in a data structure # available somewhere. This mean that route discovery is only available through the rack map, # but this is limited as not showing the actual method (GET, POST, etc...). For now The Agent # will use only the current_route_coverage for Rack applications. # # @return [Array<Contrast::Agent::Reporting::DiscoveredRoute>] # @raise [NoMethodError] raises error if subclass does not implement this method def collect_routes # return Contrast::Utils::ObjectShare::EMPTY_ARRAY unless defined?(Rack) # Rack::URLMap is used for mapping different rack apps to different paths. # The Rack app could be separated into smaller rack applications. # Rack::Builder is another option. # return Contrast::Utils::ObjectShare::EMPTY_ARRAY unless rack_map # This method is disabled for now, as it is not returning the actual routes. Code is left for as # comment for future reference. # # routes = [] # rack_map.any? do |path, meta| # routes << Contrast::Agent::Reporting::DiscoveredRoute.from_rack_route(meta[1], meta[0], path) # end # routes Contrast::Utils::ObjectShare::EMPTY_ARRAY end # Given the current request - return a RouteCoverage object # @param request [Contrast::Agent::Request] a contrast tracked request. # @param _controller [::Sinatra::Base] optionally use this controller instead of global ::Sinatra::Base. # @return [Contrast::Agent::Reporting::RouteCoverage, nil] the route coverage object or nil if no route def current_route_coverage request, _controller = nil, full_route = nil method = request.env[::Rack::REQUEST_METHOD] # GET, PUT, POST, etc... full_route ||= request.env.fetch(::Rack::PATH_INFO, nil) full_route = request.env.fetch(RACK_REQUEST_PATH, nil) if Contrast::Utils::DuckUtils.empty_duck?(full_route) return unless method # If we are here and have method but the route is "" we might be expecting the home page. full_route = '/' if Contrast::Utils::DuckUtils.empty_duck?(full_route) && request.env.fetch(RACK_SERVER_NAME, nil) route_coverage = Contrast::Agent::Reporting::RouteCoverage.new # We might not have controller, or even if there is defined one, it could not bare the name of the # route to match as an object, it could be one router class with base controller with several methods # describing each class, search for final controller might be resource heavy, and not efficient. # For now to identify the controller the Agent will use the route name, this may lead to recording # of false routes, but it is better than nothing. If route do no match a pattern it is a good practice # to notify the user by displaying a not found page, in a sense this is a exercise of the application, but # not correctly recorded controller name. Try to see if there is a define Rack::URLMap, and use it first. mapped_controller = rack_map[full_route]&.last final_controller = mapped_controller || full_route route_coverage.attach_rack_based_data(final_controller, method, nil, full_route) route_coverage end # Try and get map of Rack application { "path" => ["pattern", "controller"] }. # # @return [Hash<String, Array<String>>] the rack map def rack_map rack_map = {} maps = ObjectSpace.each_object(::Rack::URLMap).to_a maps.any? do |map| mapping = map.instance_variable_get(:@mapping) mapping.any? do |arr| path = arr[1] pattern = arr[2] controller = arr[3]&.cs__class&.cs__name rack_map[path] = [pattern, controller] if path&.cs__is_a?(String) && controller end end rack_map rescue StandardError {} end def retrieve_request env ::Rack::Request.new(env) end end end end end end