conditional proxy

This commit is contained in:
Nigel Brookes-Thomas 2020-03-12 16:37:13 +00:00
parent 0a10320fa7
commit d73f0761a8
10 changed files with 117 additions and 48 deletions

19
.rubocop.yml Normal file
View File

@ -0,0 +1,19 @@
Layout/SpaceInsideParens:
Enabled: false
Metrics/LineLength:
Max: 120
Metrics/ModuleLength:
Exclude:
- "**/*_spec.rb"
Metrics/BlockLength:
Exclude:
- "**/*_spec.rb"
Layout/TrailingWhitespace:
Enabled: false
Style/Semicolon:
Enabled: false

View File

@ -1,7 +1,9 @@
source "https://rubygems.org" # frozen_string_literal: true
source 'https://rubygems.org'
# Specify your gem's dependencies in roda-proxy.gemspec # Specify your gem's dependencies in roda-proxy.gemspec
gemspec gemspec
gem "rake", "~> 12.0" gem 'rake', '~> 12.0'
gem "rspec", "~> 3.0" gem 'rspec', '~> 3.0'

View File

@ -1,6 +1,8 @@
require "bundler/gem_tasks" # frozen_string_literal: true
require "rspec/core/rake_task"
require 'bundler/gem_tasks'
require 'rspec/core/rake_task'
RSpec::Core::RakeTask.new(:spec) RSpec::Core::RakeTask.new(:spec)
task :default => :spec task default: :spec

View File

@ -1,7 +1,8 @@
#!/usr/bin/env ruby #!/usr/bin/env ruby
# frozen_string_literal: true
require "bundler/setup" require 'bundler/setup'
require "roda/proxy" require 'roda/proxy'
# You can add fixtures and/or initialization code here to make experimenting # You can add fixtures and/or initialization code here to make experimenting
# with your gem easier. You can also use a different console, if you like. # with your gem easier. You can also use a different console, if you like.
@ -10,5 +11,5 @@ require "roda/proxy"
# require "pry" # require "pry"
# Pry.start # Pry.start
require "irb" require 'irb'
IRB.start(__FILE__) IRB.start(__FILE__)

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
require './addressing' require './addressing'
run App run App

View File

@ -1,10 +1,22 @@
require "faraday" # frozen_string_literal: true
require "roda/proxy/version"
require 'faraday'
require 'roda/proxy/version'
# :nodoc:
class Roda class Roda
# :nodoc:
module RodaPlugins module RodaPlugins
# Roda plugin for simple API proxying
module Proxy module Proxy
# Respond to the configure method to set the destination when proxying
# Expects the following options:
# [to] Required. The scheme and host of the proxy. Should not end with a slash.
# [path] Optional. The path to append to the above for proxying.
# Should begin with a +/+. Defaults to +/+.
# Example:
# plugin :proxy, to: 'https://foo.bar', path: '/my/api'
def self.configure(app, opts = {}) def self.configure(app, opts = {})
app.opts[:proxy_to] = opts.fetch(:to, nil) app.opts[:proxy_to] = opts.fetch(:to, nil)
app.opts[:proxy_path] = opts.fetch(:path, '/') app.opts[:proxy_path] = opts.fetch(:path, '/')
@ -12,45 +24,67 @@ class Roda
raise 'Proxy host not set, use "plugin :proxy, to: http://example.com"' unless app.opts[:proxy_to] raise 'Proxy host not set, use "plugin :proxy, to: http://example.com"' unless app.opts[:proxy_to]
end end
# :nodoc:
module RequestMethods module RequestMethods
# Proxies the request, forwarding all headers except +Host+ which is
# rewritten to be the destination host. The response headers, body and
# status are returned to the client.
def proxy def proxy
#pp @_request.env
#pp @_request.body.read
method = Faraday.method(env['REQUEST_METHOD'].downcase.to_sym) method = Faraday.method(env['REQUEST_METHOD'].downcase.to_sym)
pp _proxy_url
pp _proxy_headers
pp roda_class
f_response = method.call(_proxy_url) { |req| _proxy_request(req) } f_response = method.call(_proxy_url) { |req| _proxy_request(req) }
response.status = f_response.status _respond(f_response)
f_response.headers.each { |k,v| response[k] = v } end
response.write(f_response.body)
halt # Conditionally proxies when +condition+ is true and with selective probability.
# For instance, to proxy 50% of the time:
# r.proxy_when(probability: 0.5)
# Condition can be a truthy value or a block / lambda, in which case
# the result from the +#call+ is expected to be truthy.
# r.proxy_when( r.env['HTTP_PROXY_ME'] == 'true' )
# The two parameters can be combined, the probability is evaluated first.
# r.proxy_when( r.env['HTTP_PROXY_ME'] == 'true', probability: 0.8 )
# If and only if this method choses not to proxy is the block evaluated.
# The block is then expected to return a meaningful response to Roda.
def proxy_when(condition = true, probability: 1.0)
shall_proxy = Random.rand(0.0..1.0) <= probability
if shall_proxy && ( condition.respond_to?(:call) ? condition.call : condition )
yield(self)
else
proxy
end
end end
private private
def _proxy_url def _proxy_url
@_proxy_uri ||= URI(roda_class.opts[:proxy_to]) @_proxy_url ||= URI(roda_class.opts[:proxy_to])
.then { |uri| uri.path = roda_class.opts[:proxy_path] ; uri } .then { |uri| uri.path = roda_class.opts[:proxy_path]; uri }
.then { |uri| uri.query = env['QUERY_STRING'] ; uri } .then { |uri| uri.query = env['QUERY_STRING']; uri }
end end
def _proxy_headers def _proxy_headers
env env
.select { |k,_v| k.start_with? 'HTTP_'} .select { |k, _v| k.start_with? 'HTTP_' }
.reject { |k,_v| k == 'HTTP_HOST' } .reject { |k, _v| k == 'HTTP_HOST' }
.transform_keys do |k| .transform_keys do |k|
k.sub(/^HTTP_/, '') k.sub(/^HTTP_/, '')
.split('_') .split('_')
.map(&:capitalize) .map(&:capitalize)
.join('-') .join('-')
end.merge({ 'Host' => "#{_proxy_url.host}:#{_proxy_url.port}" }) end.merge({ 'Host' => "#{_proxy_url.host}:#{_proxy_url.port}" })
end end
def _proxy_request(req) def _proxy_request(req)
req.headers = _proxy_headers req.headers = _proxy_headers
end end
def _respond(proxied_response)
response.status = proxied_response.status
proxied_response.headers.each { |k, v| response[k] = v }
response.write(proxied_response.body)
end
end end
end end

View File

@ -1,5 +1,7 @@
# frozen_string_literal: true
class Roda class Roda
module Proxy module Proxy
VERSION = "0.1.0" VERSION = '0.1.0'
end end
end end

View File

@ -1,34 +1,37 @@
# frozen_string_literal: true
require_relative 'lib/roda/proxy/version' require_relative 'lib/roda/proxy/version'
Gem::Specification.new do |spec| Gem::Specification.new do |spec|
spec.name = "roda-proxy" spec.name = 'roda-proxy'
spec.version = Roda::Proxy::VERSION spec.version = Roda::Proxy::VERSION
spec.authors = ["Nigel Brookes-Thomas"] spec.authors = ['Nigel Brookes-Thomas']
spec.email = ["nigel@brookes-thomas.co.uk"] spec.email = ['nigel@brookes-thomas.co.uk']
spec.summary = 'Proxy service for Roda' spec.summary = 'Proxy service for Roda'
spec.description = 'Roda proxy service' spec.description = 'Roda proxy service'
spec.homepage = "http://foo.bar" spec.homepage = 'http://foo.bar'
spec.license = "MIT" spec.license = 'MIT'
spec.required_ruby_version = Gem::Requirement.new(">= 2.3.0") spec.required_ruby_version = Gem::Requirement.new('>= 2.3.0')
spec.metadata["allowed_push_host"] = "TODO: Set to 'http://mygemserver.com'" spec.metadata['allowed_push_host'] = "TODO: Set to 'http://mygemserver.com'"
spec.metadata["homepage_uri"] = spec.homepage spec.metadata['homepage_uri'] = spec.homepage
spec.metadata["source_code_uri"] = "http://foo.bar" spec.metadata['source_code_uri'] = 'http://foo.bar'
spec.metadata["changelog_uri"] = "http://foo.bar" spec.metadata['changelog_uri'] = 'http://foo.bar'
# Specify which files should be added to the gem when it is released. # Specify which files should be added to the gem when it is released.
# The `git ls-files -z` loads the files in the RubyGem that have been added into git. # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do spec.files = Dir.chdir(File.expand_path(__dir__)) do
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
end end
spec.bindir = "exe" spec.bindir = 'exe'
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
spec.require_paths = ["lib"] spec.require_paths = ['lib']
spec.add_dependency 'faraday', '~> 1.0' spec.add_dependency 'faraday', '~> 1.0'
spec.add_dependency 'roda', '~> 3.0' spec.add_dependency 'roda', '~> 3.0'
spec.add_development_dependency 'rerun', '~> 0.13' spec.add_development_dependency 'rerun', '~> 0.13'
spec.add_development_dependency 'rubocop', '~> 0.80'
end end

View File

@ -1,9 +1,11 @@
# frozen_string_literal: true
RSpec.describe Roda::Proxy do RSpec.describe Roda::Proxy do
it "has a version number" do it 'has a version number' do
expect(Roda::Proxy::VERSION).not_to be nil expect(Roda::Proxy::VERSION).not_to be nil
end end
it "does something useful" do it 'does something useful' do
expect(false).to eq(true) expect(false).to eq(true)
end end
end end

View File

@ -1,9 +1,11 @@
require "bundler/setup" # frozen_string_literal: true
require "roda/proxy"
require 'bundler/setup'
require 'roda/proxy'
RSpec.configure do |config| RSpec.configure do |config|
# Enable flags like --only-failures and --next-failure # Enable flags like --only-failures and --next-failure
config.example_status_persistence_file_path = ".rspec_status" config.example_status_persistence_file_path = '.rspec_status'
# Disable RSpec exposing methods globally on `Module` and `main` # Disable RSpec exposing methods globally on `Module` and `main`
config.disable_monkey_patching! config.disable_monkey_patching!