class Rack::WWWhisper
Communicates with the wwwhisper service to authorize each incoming request. Acts as a proxy for requests to locations handled by wwwhisper (/wwwhisper/auth and /wwwhisper/admin)
For each incoming request an authorization query is sent. The query contains a normalized path that a request is trying to access and wwwhisper session cookies. The query result determines the action to be performed:
- 200
-
request is allowed and passed down the
Rack
stack. - 401
-
the user is not authenticated, request is denied, login page is returned.
- 403
-
the user is not authorized, request is denied, error is returned.
- any other
-
error while communicating with wwwhisper, request is denied.
This class is thread safe, it can handle multiple simultaneous requests.
Public Class Methods
# File lib/rack/wwwhisper.rb, line 66 def self.call(env) # Delay check for WWWHISPER_DISABLE until the first # request. This way Rails assets pipeline does not fail if # environment variables are not set (as is the case on # Heroku). if ENV['WWWHISPER_DISABLE'] != '1' raise(StandardError, 'WWWHISPER_URL nor WWWHISPER_DISABLE environment variable set') end @app.call(env) end
Following environment variables are recognized:
-
WWWHISPER_DISABLE: useful for a local development environment.
-
WWWHISPER_URL: an address of a wwwhisper service that must be set if WWWHISPER_DISABLE is not set. The url includes credentials that identify a protected site. If the same credentials are used for www.example.org and www.example.com, the sites are treated as one: access control rules defined for one site, apply to the other site.
-
WWWHISPER_IFRAME: an HTML snippet to be injected into returned HTML documents (has a default value).
# File lib/rack/wwwhisper.rb, line 63 def initialize(app) @app = app if not ENV['WWWHISPER_URL'] def self.call(env) # Delay check for WWWHISPER_DISABLE until the first # request. This way Rails assets pipeline does not fail if # environment variables are not set (as is the case on # Heroku). if ENV['WWWHISPER_DISABLE'] != '1' raise(StandardError, 'WWWHISPER_URL nor WWWHISPER_DISABLE environment variable set') end @app.call(env) end return end @app = NoPublicCache.new(app) # net/http/persistent connections are thread safe. @http = http_init() @wwwhisper_uri = parse_uri(ENV['WWWHISPER_URL']) @wwwhisper_iframe = ENV['WWWHISPER_IFRAME'] || sprintf(@@DEFAULT_IFRAME, wwwhisper_path('auth/iframe.js')) @wwwhisper_iframe_bytesize = @wwwhisper_iframe.bytesize end
Public Instance Methods
Exposed for tests.
# File lib/rack/wwwhisper.rb, line 97 def auth_query(queried_path) wwwhisper_path "auth/api/is-authorized/?path=#{queried_path}" end
# File lib/rack/wwwhisper.rb, line 101 def call(env) req = Request.new(env) normalize_path(req) # Requests to /@@WWWHISPER_PREFIX/auth/ should not be authorized, # every visitor can access login pages. return dispatch(req) if req.path =~ %r{^#{wwwhisper_path('auth')}} debug req, "sending auth request for #{req.path}" auth_resp = auth_request(req) if auth_resp.code == '200' debug req, 'access granted' user = auth_resp['User'] env['REMOTE_USER'] = user if user status, headers, body = dispatch(req) if should_inject_iframe(status, headers) body = inject_iframe(headers, body) end headers['User'] = user if user [status, headers, body] else debug req, { '401' => 'user not authenticated', '403' => 'access_denied', }[auth_resp.code] || 'auth request failed' sub_response_to_rack(req, auth_resp) end end
Exposed for tests.
# File lib/rack/wwwhisper.rb, line 92 def wwwhisper_path(suffix) "#{@@WWWHISPER_PREFIX}#{suffix}" end
Private Instance Methods
# File lib/rack/wwwhisper.rb, line 235 def auth_request(req) auth_req = sub_request_init(req, 'Get', auth_query(req.path)) @http.request(@wwwhisper_uri, auth_req) end
# File lib/rack/wwwhisper.rb, line 186 def copy_body(rack_req, sub_req) if sub_req.request_body_permitted? and rack_req.body and (rack_req.content_length or rack_req.env['HTTP_TRANSFER_ENCODING'] == 'chunked') sub_req.body_stream = rack_req.body sub_req.content_length = rack_req.content_length if rack_req.content_length # Pass Content-Type header with requests that have body. sub_req.content_type = rack_req.content_type if rack_req.content_type end end
# File lib/rack/wwwhisper.rb, line 174 def copy_headers(env, sub_req) @@FORWARDED_HEADERS.each do |header| key = "HTTP_#{header.upcase}".gsub(/-/, '_') value = env[key] if value and key == 'HTTP_COOKIE' # Pass only wwwhisper's cookies to the wwwhisper service. value = value.scan(/#{@@AUTH_COOKIES_PREFIX}-[^;]*(?:;|$)/).join(' ') end sub_req[header] = value if value and not value.empty? end end
# File lib/rack/wwwhisper.rb, line 132 def debug(req, message) req.logger.debug "wwwhisper #{message}" if (req.respond_to?(:logger) && req.logger) end
# File lib/rack/wwwhisper.rb, line 268 def dispatch(orig_req) if orig_req.path =~ %r{^#{@@WWWHISPER_PREFIX}} debug orig_req, "passing request to wwwhisper service #{orig_req.path}" method = orig_req.request_method.capitalize sub_req = sub_request_init(orig_req, method, orig_req.fullpath) copy_body(orig_req, sub_req) sub_resp = @http.request(@wwwhisper_uri, sub_req) sub_response_to_rack(orig_req, sub_resp) else debug orig_req, 'passing request to Rack stack' @app.call(orig_req.env) end end
# File lib/rack/wwwhisper.rb, line 144 def http_init() http = Net::HTTP::Persistent.new(name: 'wwwhisper') store = OpenSSL::X509::Store.new() store.set_default_paths http.cert_store = store http.verify_mode = OpenSSL::SSL::VERIFY_PEER return http end
# File lib/rack/wwwhisper.rb, line 252 def inject_iframe(headers, body) total = [] body.each { |part| total << part } body.close if body.respond_to?(:close) total = total.join() if idx = total.rindex('</body>') total.insert(idx, @wwwhisper_iframe) headers['Content-Length'] &&= (headers['Content-Length'].to_i + @wwwhisper_iframe_bytesize).to_s end [total] end
# File lib/rack/wwwhisper.rb, line 153 def normalize_path(req) req.script_name = Addressable::URI.normalize_path(req.script_name).squeeze('/') req.path_info = Addressable::URI.normalize_path(req.path_info).squeeze('/') # Avoid /foo/ and /bar being combined into /foo//bar req.script_name.chomp!('/') if req.path_info[0] == ?/ end
# File lib/rack/wwwhisper.rb, line 136 def parse_uri(uri) parsed_uri = Addressable::URI.parse(uri) # If port is not specified, net/http/persistent uses port 80 for # https connections which is counter-intuitive. parsed_uri.port ||= parsed_uri.default_port parsed_uri end
# File lib/rack/wwwhisper.rb, line 240 def should_inject_iframe(status, headers) # Do not attempt to inject iframe if result is already chunked, # compressed or checksummed. (status == 200 and headers['Content-Type'] =~ /text\/html/i and not headers['Transfer-Encoding'] and not headers['Content-Range'] and not headers['Content-Encoding'] and not headers['Content-MD5'] ) end
# File lib/rack/wwwhisper.rb, line 162 def sub_request_init(rack_req, method, path) sub_req = Net::HTTP.const_get(method).new(path) copy_headers(rack_req.env, sub_req) scheme = rack_req.env['HTTP_X_FORWARDED_PROTO'] ||= rack_req.scheme sub_req['Site-Url'] = "#{scheme}://#{rack_req.env['HTTP_HOST']}" sub_req['User-Agent'] = "Ruby-#{Rack::WWWHISPER_VERSION}" if @wwwhisper_uri.user and @wwwhisper_uri.password sub_req.basic_auth(@wwwhisper_uri.user, @wwwhisper_uri.password) end sub_req end
# File lib/rack/wwwhisper.rb, line 198 def sub_response_headers_to_rack(rack_req, sub_resp) cookies = sub_resp.get_fields('Set-Cookie') if Rack.const_defined?('Headers') # Rack 3+ rack_headers = Rack::Headers.new() # Multiple Set-Cookie headers need to be set as an array (new # Rack SPEC) rack_headers['Set-Cookie'] = cookies if cookies else # Older Rack versions rack_headers = Rack::Utils::HeaderHash.new() # Multiple Set-Cookie headers need to be set as a single value # separated by \n (old Rack SPEC) rack_headers['Set-Cookie'] = cookies.join("\n") if cookies end sub_resp.each_capitalized do |header, value| # If sub request returned chunked response, remove the header # (chunks will be combined and returned with 'Content-Length). rack_headers[header] = value if header !~ /Transfer-Encoding|Set-Cookie/ end return rack_headers end
# File lib/rack/wwwhisper.rb, line 221 def sub_response_to_rack(rack_req, sub_resp) code = sub_resp.code.to_i headers = sub_response_headers_to_rack(rack_req, sub_resp) body = sub_resp.read_body() || '' if code < 200 or [204, 205, 304].include?(code) # To make sure Rack SPEC is respected. headers.delete('Content-Length') headers.delete('Content-Type') elsif (body.length || 0) != 0 and not headers['Content-Length'] headers['Content-Length'] = body.bytesize.to_s end [ code, headers, [body] ] end