Module: Roda::RodaPlugins::HostRouting

Defined in:
lib/roda/plugins/host_routing.rb

Overview

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 host header 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 forwarded and x-forwarded-host headers, will check both by default, and the order of checking depends on Rack's configuration (this can be configured using Rack::Request.forwarded_priority).
  • If the application can be reached via an arbitrary host header, the attacker has full control over the Rack::Request#host value 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.

Defined Under Namespace

Modules: InstanceMethods, RequestMethods

Constant Summary collapse

REQUEST_INSTANCE_VARIABLES =
[:@_host_routing_host].freeze

Class Method Summary collapse

Class Method Details

.configure(app, opts = OPTS, &block) ⇒ Object

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.



102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
# File 'lib/roda/plugins/host_routing.rb', line 102

def self.configure(app, opts=OPTS, &block)
  hosts, host_hash, default_block, default_host = DSL.new.process(&block)
  app.opts[:host_routing_hash] = host_hash
  app.opts[:host_routing_default_host] = default_host

  app.send(:define_method, :_host_routing_default, &default_block) if default_block

  app::RodaRequest.class_exec do
    hosts.each do |host|
      host_sym = host.to_sym
      define_method(host_sym){|&blk| always(&blk) if _host_routing_host == host}
      alias_method host_sym, host_sym

      meth = :"#{host}?"
      define_method(meth){_host_routing_host == host}
      alias_method meth, meth
    end
  end

  if opts[:scope_predicates]
    app.class_exec do
      hosts.each do |host|
        meth = :"#{host}?"
        define_method(meth){@_request.send(meth)}
        alias_method meth, meth
      end
    end
  end
end