require_relative "vm" module Immunio class Request # Data sent to the server attr_accessor :data, :vm # We keep the VM and hook handlers in the request to ensure all hooks for this request # use the same handlers. attr_accessor :vm def initialize(env) @env = env @data = { "type" => "engine.http_request", "request_id" => id, "start_time" => Time.now.utc.iso8601(6), } @timings = Hash.new { |h, name| h[name] = { count: 0, total_duration: 0 } } @paused_times = {} @start_time = Time.now Immunio.logger.debug { "Created new Request #{id} with data #{@data}" } end def id @env["action_dispatch.request_id"] || internal_id end # Time the execution of a block and stores it in the request's `timings`. def time(type, name) Immunio.logger.debug { "Starting request time for #{type} #{name}" } start = Time.now key = {type: type, name: name} nested = false if @paused_times[key] nested = true else @paused_times[key] = 0 end yield ensure timing = @timings[key] timing[:count] += 1 timing[:total_duration] += (Time.now - start) * 1000 if !nested timing[:total_duration] -= @paused_times.delete(key) end end # Pause execution timing while executing block def pause(type, name) key = {type: type, name: name} start = Time.now yield ensure @paused_times[key]+= (Time.now - start) * 1000 end def timings total_key = {type: "request", name: "total"} timing = @timings[total_key] timing[:count] = 1 timing[:total_duration] = (Time.now - @start_time) * 1000 split_timings = {} @timings.each do |key, value| # Round to microsecond precision value[:total_duration] = value[:total_duration].round(3) split_timings[key[:type]] ||= {} split_timings[key[:type]][key[:name]] = value end split_timings end def should_report? result = Immunio.run_hook "request", "should_report" # If the hook doesn't exist, turn a nil report value into a false value report = !!result["report"] Immunio.logger.debug { "Report request: #{report}" } report end # Encode the request data to send it to the channel. def encode if @data.is_a? Hash @data.to_msgpack else # Lua table # Encode the data in Lua to avoid pulling a huge Lua table out of the VM, which is slow. @vm.unsafe_call_by_name 'encode', @data end end def self.current Thread.current["immunio.request"] end def self.current=(request) Thread.current["immunio.request"] = request Immunio.logger.debug { "Setting current request for thread #{Thread.current.object_id} to #{request && request.id || nil}" } end # This shim exists to preserve the stack depth into hooks when self.time and self.pause are # called. Otherwise the number of immunio frames will vary based on the state of the request object def self.stack_shim() yield end def self.time(type, name, &block) if Request.current Request.current.time(type, name, &block) else Immunio.logger.debug { "Starting request time for #{type} #{name}" } self.stack_shim(&block) end end def self.pause(type, name, &block) if Request.current Request.current.pause(type, name, &block) else self.stack_shim(&block) end end private def internal_id return @internal_id if defined?(@internal_id) if @env['HTTP_X_REQUEST_ID'] @internal_id = @env['HTTP_X_REQUEST_ID'].gsub(/[^\w\-]/, '').first(255) else @internal_id = SecureRandom.uuid end end end end