# Copyright (c) 2022 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details. # frozen_string_literal: true require 'net/http' require 'contrast/utils/net_http_base' require 'contrast/components/logger' require 'contrast/utils/object_share' require 'contrast/agent/version' require 'json' module Contrast module Utils # This module creates a Net::HTTP client and initiates a connection to the provided result class TelemetryClient < NetHttpBase ENDPOINT = 'api/v1/telemetry/metrics' # /TelemetryEvent.path EXCEPTIONS = 'api/v1/telemetry/exceptions' # /TelemetryExceptions::Event.path SERVICE_NAME = 'Telemetry' include Contrast::Components::Logger::InstanceMethods # This method initializes the Net::HTTP client we'll need. it will validate # the connection and make the first request. If connection is valid and response # is available then the open connection is returned. # # @param url [String] # @return [Net::HTTP, nil] Return open connection or nil def initialize_connection url super(SERVICE_NAME, url, use_proxy: false, use_custom_cert: false) end # This method will be responsible for building the request. Because the telemetry collector expects to receive # multiple events in a single request, we must always wrap the event in an array, even if there is only one. # # @param event [Contrast::Agent::Telemetry::Event, Array] # @return [Net::HTTP::Post] def build_request event return unless valid_event? event string_body = if Array(event).all?(Contrast::Agent::Telemetry::TelemetryException::Event) event.map(&:to_controlled_hash).flatten! else [event.to_hash] end header = { 'User-Agent' => "<#{ Contrast::Utils::ObjectShare::RUBY }>-<#{ Contrast::Agent::VERSION }>", 'Content-Type' => 'application/json' } request = Net::HTTP::Post.new(build_path(event), header) request.body = string_body.to_json request end # This method will create the actual request and send it # @param event[Contrast::Agent::Telemetry::Event] # @param connection[Net::HTTP] def send_request event, connection return if connection.nil? || event.nil? return unless valid_event? event req = build_request event connection.request req end # This method will handle the response from the tenant # @param res [Net::HTTPResponse] # @return sleep_time [Integer, nil] def handle_response res status_code = res.code.to_i ready_after = if res.to_hash.keys.map(&:downcase).include?('ready-after') res['Ready-After'] else 60 end ready_after if status_code == 429 end # This method will be responsible for validating the event # @param event[Contrast::Agent::Telemetry::Event,Contrast::Agent::Telemetry::StartupMetricsEvent, # array] def valid_event? event return true if event.cs__is_a?(Contrast::Agent::Telemetry::Event) return true if event.cs__is_a?(Contrast::Agent::Telemetry::StartupMetricsEvent) # Batch return true if Array(event).all?(Contrast::Agent::Telemetry::TelemetryException::Event) false end private # The telemetry instance accepts any path to #{ Contrast::Agent::Telemetry::URL }#{ ENDPOINT }, using the # remainder of the path to segregate messages. # # @param event [Contrast::Agent::Telemetry::Event, Contrast::Agent::Telemetry::TelemetryException::Event] # @return [String] the fully qualified path to send the request def build_path event endpoint = Array(event).all?(Contrast::Agent::Telemetry::TelemetryException::Event) ? EXCEPTIONS : ENDPOINT path = endpoint == EXCEPTIONS ? Contrast::Agent::Telemetry::TelemetryException::Event.path : event.path "#{ Contrast::Agent::Telemetry::Base::URL }#{ endpoint }#{ path }" end end end end