# frozen_string_literal: true # # ronin-web-server - A custom Ruby web server based on Sinatra. # # Copyright (c) 2006-2023 Hal Brodigan (postmodern.mod3 at gmail.com) # # ronin-web-server is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published # by the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # ronin-web-server is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License # along with ronin-web-server. If not, see . # require 'ronin/web/server/reverse_proxy/request' require 'ronin/web/server/reverse_proxy/response' require 'ronin/support/network/http' require 'rack' module Ronin module Web module Server # # Reverse proxies Rack requests to other HTTP web servers. # # ## Examples # # ### Standalone Server # # reverse_proxy = Ronin::Web::Server::ReverseProxy.new do |proxy| # proxy.on_request do |request| # # ... # end # # proxy.on_response do |response| # # ... # end # end # reverse_proxy.run!(host: '0.0.0.0', port: 8080) # # ### App # # class App < Ronin::Web::Server::Base # # mount '/signin', Ronin::Web::Server::ReverseProxy.new # # end # # @api public # class ReverseProxy # # Creates a new reverse proxy application. # # @yield [reverse_proxy] # If a block is given, it will be passed the new proxy. # # @yieldparam [ReverseProxy] proxy # The new proxy object. # def initialize @connections = {} yield self if block_given? end # # Registers a callback to run on each request. # # @yield [request] # The given block will be passed each received request before it has # been reverse proxied. # def on_request(&block) @on_request_callback = block end # # Registers a callback to run on each response. # # @yield [response] # The given block will be passed each response before it has been # returned. # # @yield [request, response] # If the block accepts two arguments then both the request and the # response objects will be yielded. # # @yieldparam [ReverseProxy::Response] response # A response object. # # @yieldparam [ReverseProxy::Request] request # A request object. # def on_response(&block) @on_response_callback = block end # # Reverse proxies every request using the `Host` header. # # @param [Hash{String => Object}] env # The rack request env Hash. # # @return [(Integer, Hash{String => String}, Array)] # The rack response tuple (status, headers, body). # def call(env) request = Request.new(env) @on_request_callback.call(request) if @on_request_callback response = reverse_proxy(request) if @on_response_callback if @on_response_callback.arity == 1 @on_response_callback.call(response) else @on_response_callback.call(request,response) end end return [response.status, response.headers, response.body] end # # Creates a new connection or fetches an existing connection. # # @param [String] host # The host to connect to. # # @param [Integer] port # The port to connect to. # # @param [Boolean] ssl # Indicates whether to use SSL. # # @return [Ronin::Support::Network::HTTP] # The HTTP connection. # # @api private # def connection_for(host,port, ssl: nil) key = [host,port,ssl] @connections.fetch(key) do @connections[key] = Support::Network::HTTP.new(host,port, ssl: ssl) end end # Blacklisted HTTP response Headers. IGNORED_HEADERS = Set[ 'Transfer-Encoding' ] # # Reverse proxies the given request. # # @param [ReverseProxy::Request] request # The incoming request to reverse proxy. # # @return [ReverseProxy::Response] # The reverse proxied response. # def reverse_proxy(request) host = request.host port = request.port ssl = request.scheme == 'https' method = request.request_method.downcase.to_sym path = request.path query = request.query_string headers = request.headers body = request.body.read http = connection_for(host,port, ssl: ssl) http_response = http.request(method,path, query: query, headers: headers, body: body) response_headers = {} http_response.each_capitalized do |name,value| unless IGNORED_HEADERS.include?(name) response_headers[name] = value end end response_body = http_response.body || '' response_status = http_response.code.to_i return Response.new(response_body,response_status,response_headers) end # # @group Standalone Server Methods # # Default host the Proxy will bind to DEFAULT_HOST = '0.0.0.0' # Default port the Proxy will listen on DEFAULT_PORT = 8080 # Default server the Proxy will run on DEFAULT_SERVER = 'webrick' # # Runs the reverse proxy as a standalone HTTP server. # # @param [String] host # The host to bind to. # # @param [Integer] port # The port to listen on. # # @param [String] server # The Rack server to run the reverse proxy under. # # @param [Hash{Symbol => Object}] rack_options # Additional options to pass to [Rack::Server.new](https://rubydoc.info/gems/rack/Rack/Server#initialize-instance_method). # def run!(host: DEFAULT_HOST, port: DEFAULT_PORT, server: DEFAULT_SERVER, **rack_options) server = Rack::Server.new( app: self, server: server, Host: host, Port: port, **rack_options ) server.start do |handler| trap(:INT) { quit!(server,handler) } trap(:TERM) { quit!(server,handler) } end return self end # # Stops the reverse proxy server. # # @param [Rack::Server] server # The Rack Handler server. # # @param [#stop!, #stop] handler # The Rack Handler. # # @api private # def quit!(server,handler) # Use thins' hard #stop! if available, otherwise just #stop handler.respond_to?(:stop!) ? handler.stop! : handler.stop end end end end end