# frozen_string_literal: true require_relative '../../middlewares/memory_invalidation_middleware' require_relative '../../middlewares/rate_limiter_middleware' require_relative '../../data_filters/response_data_filter' require_relative '../../errors/too_many_requests_error' require_relative '../../utils/supported_ssl_versions' require_relative '../../aspects/prometheus_aspect' require_relative '../../aspects/logging_aspect' require_relative '../../aspects/cache_aspect' require 'securerandom' ## # Base module for Server classes. It contains # methods for client handling, error handling, # set features and every method that is common # for the implementations of Servers. module ServerBase prepend CacheAspect prepend LoggingAspect prepend PrometheusAspect private def call_endpoint(name, client_data, session_id, _client_ip) @macaw.send( name.to_sym, { headers: client_data[:headers], body: client_data[:body], params: client_data[:params], client: @session&.dig(session_id)&.dig(0) } ) end def get_client_data(body, headers, parameters) { body: body, headers: headers, params: parameters } end def handle_client(client) _path, method_name, headers, body, parameters = RequestDataFiltering.parse_request_data(client, @macaw.routes) raise EndpointNotMappedError unless @macaw.respond_to?(method_name) raise TooManyRequestsError unless @rate_limit.nil? || @rate_limit.allow?(client.peeraddr[3]) client_data = get_client_data(body, headers, parameters) session_id = declare_client_session(client_data[:headers], @macaw.secure_header) if @macaw.session message, status, response_headers = call_endpoint(@prometheus_middleware, @macaw_log, @cache, method_name, client_data, session_id, client.peeraddr[3]) response_headers ||= {} response_headers[@macaw.secure_header] = session_id if @macaw.session status ||= 200 message ||= nil response_headers ||= nil client.puts ResponseDataFilter.mount_response(status, response_headers, message) rescue IOError, Errno::EPIPE => e @macaw_log&.error("Error writing to client: #{e.message}") rescue TooManyRequestsError client.print "HTTP/1.1 429 Too Many Requests\r\n\r\n" rescue EndpointNotMappedError client.print "HTTP/1.1 404 Not Found\r\n\r\n" rescue StandardError => e client.print "HTTP/1.1 500 Internal Server Error\r\n\r\n" @macaw_log&.error(e.full_message) ensure begin client.close rescue IOError => e @macaw_log&.error("Error closing client: #{e.message}") end end def declare_client_session(headers, secure_header_name) session_id = headers[secure_header_name] || SecureRandom.uuid session_id = SecureRandom.uuid if @session[session_id].nil? @session[session_id] ||= [{}, Time.now] session_id end def set_rate_limiting return unless @macaw.config&.dig('macaw', 'rate_limiting') @rate_limit = RateLimiterMiddleware.new( @macaw.config['macaw']['rate_limiting']['window'].to_i || 1, @macaw.config['macaw']['rate_limiting']['max_requests'].to_i || 60 ) end def set_ssl ssl_config = @macaw.config['macaw']['ssl'] if @macaw.config&.dig('macaw', 'ssl') ssl_config ||= nil unless ssl_config.nil? version_config = { min: ssl_config['min'], max: ssl_config['max'] } @context = OpenSSL::SSL::SSLContext.new @context.min_version = SupportedSSLVersions::VERSIONS[version_config[:min]] unless version_config[:min].nil? @context.max_version = SupportedSSLVersions::VERSIONS[version_config[:max]] unless version_config[:max].nil? @context.cert = OpenSSL::X509::Certificate.new(File.read(ssl_config['cert_file_name'])) if ssl_config['key_type'] == 'RSA' || ssl_config['key_type'].nil? @context.key = OpenSSL::PKey::RSA.new(File.read(ssl_config['key_file_name'])) elsif ssl_config['key_type'] == 'EC' @context.key = OpenSSL::PKey::EC.new(File.read(ssl_config['key_file_name'])) else raise ArgumentError, "Unsupported SSL/TLS key type: #{ssl_config['key_type']}" end end @context ||= nil rescue IOError => e @macaw_log&.error("It was not possible to read files #{@macaw.config['macaw']['ssl']['cert_file_name']} and #{@macaw.config['macaw']['ssl']['key_file_name']}. Please assure the files exist and their names are correct.") @macaw_log&.error(e.backtrace) raise e end def set_session return unless @macaw.session @session ||= {} inv = if @macaw.config&.dig('macaw', 'session', 'invalidation_time') MemoryInvalidationMiddleware.new(@macaw.config['macaw']['session']['invalidation_time']) else MemoryInvalidationMiddleware.new end inv.cache = @session end def set_features @is_shutting_down = false set_rate_limiting set_session set_ssl end end