The host_routing plugin adds support for more routing requests based on the requested host. It also adds predicate methods for checking whether a request was requested with the given host.
When loading the plugin, you pass a block, which is used for configuring the plugin. For example, if you want to treat requests to api.example.com or api2.example.com as api requests, and treat other requests as www requests, you could use:
plugin :host_routing do |hosts| hosts.to :api, "api.example.com", "api2.example.com" hosts.default :www end
With this configuration, in your routing tree, you can call the r.api and r.www methods for dispatching to routing blocks only for those types of requests:
route do |r| r.api do # requests to api.example.com or api2.example.com end r.www do # requests to other domains end end
In addition to the routing methods, predicate methods are also added to the request object:
route do |r| "#{r.api?}-#{r.www?}" end # Requests to api.example.com or api2.example.com return "true-false" # Other requests return "false-true"
If the :scope_predicates plugin option is given, predicate methods are also created in route block scope:
plugin :host_routing, scope_predicates: true do |hosts| hosts.to :api, "api.example.com" hosts.default :www end route do |r| "#{api?}-#{www?}" end
To handle hosts that match a certain format (such as all subdomains), where the specific host names are not known up front, you can provide a block when calling hosts.default. This block is passed the host name, or an empty string if no host name is provided, and is evaluated in route block scope. When using this support, you should also call hosts.register to register host types that could be returned by the block. For example, to handle api subdomains differently:
plugin :host_routing do |hosts| hosts.to :api, "api.example.com" hosts.register :api_sub hosts.default :www do |host| :api_sub if host.end_with?(".api.example.com") end end
This plugin uses the host method on the request to get the hostname (this method is defined by Rack). Note that as of Rack 3.2, Rack’s default approach to determine the hostname is insecure in many cases, since:
-
It will use a forwarded header instead of the
hostheader by default if the request was from a trusted proxy. -
Rack trust proxies from any private IP address range by default (this can be disabled using
Rack::Request.ip_filter=). -
Rack supports both
forwardedandx-forwarded-hostheaders, will check both by default, and the order of checking depends on Rack’s configuration (this can be configured usingRack::Request.forwarded_priority). -
If the application can be reached via an arbitrary
hostheader, the attacker has full control over theRack::Request#hostvalue even when forwarded host usage is disabled.
You should configure Rack appropriately for your deployment, not allowing the use of forwarded information unless your deployment is using a reverse proxy, and only allowing the specific forwarded header set by the expected reverse proxy if a reverse proxy is used. Rack 3.3 should include more control in this area, and Rack 4 should ship with more secure defaults in regard to forwarding headers.
It is recommended to use the host_authorization plugin to check that the requested host is allowed when using this plugin.
Classes and Modules
Constants
| REQUEST_INSTANCE_VARIABLES | = | [:@_host_routing_host].freeze |
Public Class methods
Setup the host routing support. The block yields an object used to configure the plugin. Options:
| :scope_predicates |
Setup predicate methods in route block scope in addition to request scope. |
# File lib/roda/plugins/host_routing.rb 102 def self.configure(app, opts=OPTS, &block) 103 hosts, host_hash, default_block, default_host = DSL.new.process(&block) 104 app.opts[:host_routing_hash] = host_hash 105 app.opts[:host_routing_default_host] = default_host 106 107 app.send(:define_method, :_host_routing_default, &default_block) if default_block 108 109 app::RodaRequest.class_exec do 110 hosts.each do |host| 111 host_sym = host.to_sym 112 define_method(host_sym){|&blk| always(&blk) if _host_routing_host == host} 113 alias_method host_sym, host_sym 114 115 meth = :"#{host}?" 116 define_method(meth){_host_routing_host == host} 117 alias_method meth, meth 118 end 119 end 120 121 if opts[:scope_predicates] 122 app.class_exec do 123 hosts.each do |host| 124 meth = :"#{host}?" 125 define_method(meth){@_request.send(meth)} 126 alias_method meth, meth 127 end 128 end 129 end 130 end