lib/protocol/rack/adapter/generic.rb
# frozen_string_literal: true # Copyright, 2017, by Samuel G. D. Williams. <http://www.codeotaku.com> # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. require 'console' require_relative '../constants' require_relative '../input' require_relative '../response' module Protocol module Rack module Adapter class Generic def self.wrap(app) self.new(app) end # Initialize the rack adaptor middleware. # @parameter app [Object] The rack middleware. def initialize(app) @app = app raise ArgumentError, "App must be callable!" unless @app.respond_to?(:call) end def logger Console.logger end # Unwrap raw HTTP headers into the CGI-style expected by Rack middleware. # # Rack separates multiple headers with the same key, into a single field with multiple lines. # # @parameter headers [Protocol::HTTP::Headers] The raw HTTP request headers. # @parameter env [Hash] The rack request `env`. def unwrap_headers(headers, env) headers.each do |key, value| http_key = "HTTP_#{key.upcase.tr('-', '_')}" if current_value = env[http_key] env[http_key] = "#{current_value};#{value}" else env[http_key] = value end end end # Process the incoming request into a valid rack `env`. # # - Set the `env['CONTENT_TYPE']` and `env['CONTENT_LENGTH']` based on the incoming request body. # - Set the `env['HTTP_HOST']` header to the request authority. # - Set the `env['HTTP_X_FORWARDED_PROTO']` header to the request scheme. # - Set `env['REMOTE_ADDR']` to the request remote adress. # # @parameter request [Protocol::HTTP::Request] The incoming request. # @parameter env [Hash] The rack `env`. def unwrap_request(request, env) if content_type = request.headers.delete('content-type') env[CGI::CONTENT_TYPE] = content_type end # In some situations we don't know the content length, e.g. when using chunked encoding, or when decompressing the body. if body = request.body and length = body.length env[CGI::CONTENT_LENGTH] = length.to_s end self.unwrap_headers(request.headers, env) # HTTP/2 prefers `:authority` over `host`, so we do this for backwards compatibility. env[CGI::HTTP_HOST] ||= request.authority if request.respond_to?(:remote_address) if remote_address = request.remote_address env[CGI::REMOTE_ADDR] = remote_address.ip_address if remote_address.ip? end end end # Build a rack `env` from the incoming request and apply it to the rack middleware. # # @parameter request [Protocol::HTTP::Request] The incoming request. def call(request) env = self.make_environment(request) status, headers, body = @app.call(env) return Response.wrap(status, headers, body, request) rescue => exception Console.logger.error(self) {exception} body&.close if body.respond_to?(:close) return failure_response(exception) end private # Generate a suitable response for the given exception. # @parameter exception [Exception] # @returns [Protocol::HTTP::Response] def failure_response(exception) Protocol::HTTP::Response.for_exception(exception) end end end end end