# Copyright (c) 2020 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details. # frozen_string_literal: true require 'contrast/api/dtm.pb' require 'contrast/framework/base_support' require 'contrast/framework/rails/patch/support' require 'contrast/utils/string_utils' module Contrast module Framework module Rails # Used when Rails is present to define framework specific behavior class Support extend Contrast::Framework::BaseSupport extend Contrast::Framework::Rails::Patch::Support class << self RAILS_MODULE_NAME_VERSION = Gem::Version.new('6.0.0') def detection_class 'Rails' end def version ::Rails.version end def application_name app_class = ::Rails.application.cs__class # Rails version 6.0.0 deprecated Rails::Application#parent_name, in Rails 6.1.0 that method will be removed entirely # and instead we need to use parent_module_name return app_class.parent_module_name if Gem::Version.new(::Rails.version) >= RAILS_MODULE_NAME_VERSION app_class.parent_name end def application_root ::Rails.root end def server_type 'rails' end def collect_routes find_all_routes(::Rails.application, []) end # Find the current route, based on the provided Request wrapper # # @param request[Contrast::Agent::Request] # @return [Contrast::Api::Dtm::RouteCoverage] def current_route request return unless ::Rails.cs__respond_to?(:application) match, _params, route, path = get_full_route(request.rack_request) original_url = request.rack_request.path_info # Route is either the final rails route, or a router that points to a Sinatra controller. if Contrast::Framework::Sinatra::Support.sinatra_controller?(route.app.app) # Create a request copied from current request, but with the base path removed from path_info. new_req = ::ActionDispatch::Request.new(request.env) new_req.path_info = new_req.path_info.gsub((path << match).join, '') return Contrast::Framework::Sinatra::Support.current_route(new_req, route.app.app, original_url) end Contrast::Api::Dtm::RouteCoverage.from_action_dispatch_journey(route, original_url) rescue StandardError => _e nil end # Copy a request for modification. # # @param [::ActionDispatch::Request] original env. # @return [::ActionDispatch::Request] a copy of original env with rails env merged. def retrieve_request env rails_env = ::Rails.application.env_config.merge(env) ::ActionDispatch::Request.new(rails_env || env) end AC_INSTANCE = 'action_controller.instance' def streaming? env return false unless defined?(::ActionController::Live) env[AC_INSTANCE].cs__class.included_modules.include?(::ActionController::Live) end private # Determine if route is a Rails engine route. # # @param [Object] app or route that points to a ::Rails::Engine # @return [bool] whether the router is an engine or not. def engine_route? route route.app.is_a?(::ActionDispatch::Routing::Mapper::Constraints) && route.app.app < ::Rails::Engine end # Recursively get final route traversing engines as required. # # @param request [::Rack::Request] the rack request as will be handed to rails controller. # @param top_router [::ActionDispatch::Journer::Router] the current router relative to the previous. # @param path [Array] the chunks of path that have been seen. # @return [Array] the final set of rails route classes. def get_full_route request, top_router = ::Rails.application.routes.router, path = [] return if (route_matches = top_router.send(:find_routes, request)).empty? match, params, route = route_matches.first # If the current routing node points to a sub-app (::Rais::Engine), dive deeper. # Have sub-app route the remainder of the url. if engine_route?(route) new_req = retrieve_request request.env new_req.path_info = new_req.path_info.gsub(match.to_s, '') get_full_route(new_req, route.app.app.routes.router, path << match.to_s) else [match, params, route, path] end end # Rails engine routes need to be detected by inspecting Engine class route set def find_all_routes app, route_list return route_list unless app.cs__respond_to?(:routes) && app.routes.cs__respond_to?(:routes) app.routes.routes.each do |route| if route.cs__respond_to?(:app) && route.app.cs__class == ActionDispatch::Routing::RouteSet::Dispatcher route_list << Contrast::Api::Dtm::RouteCoverage.from_action_dispatch_journey(route) elsif route.app.app.cs__respond_to?(:routes) route_list += find_all_routes(route.app.app, []) end end route_list end end end end end end