lib/lockbox_middleware.rb in lockbox_middleware-1.5.1 vs lib/lockbox_middleware.rb in lockbox_middleware-1.6.2

- old
+ new

@@ -1,38 +1,37 @@ require 'rubygems' -gem 'dnclabs-httparty' -require 'httparty' +require 'httpotato' require 'lockbox_cache' -require 'digest/md5' +require 'hmac_request' class LockBox - include HTTParty + include HTTPotato include LockBoxCache + + attr_accessor :cache + @@config = nil + @@protected_paths = nil def self.config return @@config if @@config - if defined?(Rails) - root_dir = Rails.root + #use rails config if it's there + if defined?(Rails) && Rails.root + config_file = Rails.root.join('config','lockbox.yml') + @@config = YAML.load_file(config_file)[Rails.env] else - root_dir = '.' - end - yaml_config = YAML.load_file(File.join(root_dir,'config','lockbox.yml')) - return_config = {} - environment = Rails.env if defined? Rails - environment ||= ENV['RACK_ENV'] - environment ||= 'test' - if !environment.nil? - if !yaml_config['all'].nil? + env = ENV['RACK_ENV'] || "test" + config_file = File.join(Dir.pwd, 'config','lockbox.yml') + all_configs = YAML.load_file(config_file) + if !all_configs['all'].nil? $stderr.puts "The 'all' environment is deprecated in lockbox.yml; use built-in yaml convention instead." - return_config = yaml_config['all'] - return_config.merge!(yaml_config[environment]) + @@config = all_configs['all'].merge!(all_configs[env]) else - return_config = yaml_config[environment] + @@config = all_configs[env] end end - @@config = return_config + return @@config end base_uri config['base_uri'] def initialize(app) @@ -41,68 +40,69 @@ end def call(env) dup.call!(env) end - + + def cache_string_for_key(api_key) + "lockbox_key_#{api_key}" + end + + def cache_string_for_hmac(hmac_id) + "lockbox_hmac_#{hmac_id.gsub(/[^a-z0-9]/i,'_')}" + end + def protected_paths - self.class.config['protect_paths'].map do |path| - Regexp.new(path) - end + @@protect_paths ||= self.class.config['protect_paths'].map{ |path| Regexp.new(path) } end def call!(env) - #attempt to authenticate any requests to /api - request = Rack::Request.new(env) - path_protected = false - protected_paths.each do |path| - if env['PATH_INFO'] =~ path - path_protected = true - authorized = false - key = request['key'] - if key.blank? - key = 'hmac' + protected_path = protected_paths.detect{|path| env['PATH_INFO'] =~ path} + #if the requested path is protected, it needs to be authenticated + if protected_path + request = HmacRequest.new_from_rack_env(env) + if !request['key'].nil? + auth = auth_via_key(request['key'], request) + else + auth = auth_via_hmac(request) end - - auth = auth_response(key,env) - authorized = auth[:authorized] - auth_headers = auth[:headers] - - if authorized + + if auth[:authorized] app_response = @app.call(env) - app_headers = app_response[1] - response_headers = app_headers.merge(auth_headers) - return [app_response[0], response_headers, app_response[2]] + return [app_response[0], app_response[1].merge(auth[:headers]), app_response[2]] else message = "Access Denied" return [401, {'Content-Type' => 'text/plain', 'Content-Length' => "#{message.length}"}, [message]] end - end - end - unless path_protected + else #pass everything else straight through to app return @app.call(env) end end - def auth_response(api_key, env={}) - if api_key != 'hmac' - cached_auth = auth_cache(api_key) - if !cached_auth.nil? - # currently we don't cache forward headers - return {:authorized => cached_auth, :headers => {}} - end - end - auth_response = self.class.get("/authentication/#{api_key}", {:headers => auth_headers(env), :request => {:application_name => LockBox.config['application_name']}}) + def auth_via_key(api_key, request) + cached_auth = check_key_cache(api_key) + # currently we don't cache forward headers + return {:authorized => cached_auth, :headers => {}} if cached_auth + auth_response = self.class.get("/authentication/#{api_key}", {:headers => request.get_xreferer_auth_headers, :request => {:application_name => LockBox.config['application_name']}}) authorized = (auth_response.code == 200) - cache_response_if_allowed(api_key, auth_response) if authorized + cache_key_response_if_allowed(api_key, auth_response) if authorized {:authorized => authorized, :headers => response_headers(auth_response)} end - + + def auth_via_hmac(hmac_request) + cached_auth = check_hmac_cache(hmac_request) + return {:authorized => cached_auth, :headers => {}} if cached_auth + auth_response = self.class.get("/authentication/hmac", {:headers => hmac_request.get_xreferer_auth_headers, :request => {:application_name => LockBox.config['application_name']}}) + authorized = (auth_response.code == 200) + cache_hmac_response_if_allowed(hmac_request, auth_response) if authorized + {:authorized => authorized, :headers => response_headers(auth_response)} + end + private - - def cache_response_if_allowed(api_key, auth_response) + + def cache_key_response_if_allowed(api_key, auth_response) cache_control = auth_response.headers['Cache-Control'].split(/,\s*/) cache_max_age = 0 cache_public = false cache_control.each do |c| if c =~ /^max-age=\s*(\d+)$/ @@ -111,54 +111,65 @@ cache_public = true end end caching_allowed = (cache_max_age > 0 && cache_public) expiration = Time.at(Time.now.to_i + cache_max_age) - cache_auth(api_key,expiration) if caching_allowed + @cache.write(cache_string_for_key(api_key), expiration.to_i) if caching_allowed end - def response_headers(auth_response) - headers = {} - auth_response.headers.each_pair do |h,v| - if h =~ /^X-RateLimit-/ - headers[h] = v - elsif h =~ /^X-LockBox-/ - headers[h] = v + def cache_hmac_response_if_allowed(hmac_request, auth_response) + cache_control = auth_response.headers['Cache-Control'].split(/,\s*/) + cache_max_age = 0 + cache_public = false + cache_control.each do |c| + if c =~ /^max-age=\s*(\d+)$/ + cache_max_age = $1.to_i + elsif c == 'public' + cache_public = true end end - headers + caching_allowed = (cache_max_age > 0 && cache_public) + expiration = Time.at(Time.now.to_i + cache_max_age) + if caching_allowed + api_key = auth_response.headers['X-LockBox-API-Key'] + @cache.write(cache_string_for_hmac(hmac_request.hmac_id), [api_key, expiration.to_i]) + end end - def auth_headers(env) + def response_headers(auth_response) headers = {} - headers['Referer'] = "#{env['rack.url_scheme']}://#{env['SERVER_NAME']}#{env['PATH_INFO']}" - headers['Referer'] << "?#{env['QUERY_STRING']}" unless env['QUERY_STRING'].blank? - headers['X-Referer-Content-MD5'] = Digest::MD5.hexdigest(Rack::Request.new(env).body.read) if env['CONTENT_TYPE'] - {'Content-Type' => 'CONTENT_TYPE', 'Date' => 'HTTP_DATE', 'Method' => 'REQUEST_METHOD', - 'Authorization' => 'HTTP_AUTHORIZATION'}.each_pair do |h,e| - headers["X-Referer-#{h}"] = env[e] unless env[e].blank? + auth_response.headers.each_pair do |h,v| + headers[h] = v if h =~ /^X-RateLimit-|^X-LockBox-/ end - headers["X-Referer-Date"] = env['HTTP_X_AUTHHMAC_REQUEST_DATE'] unless env['HTTP_X_AUTHHMAC_REQUEST_DATE'].blank? headers end - - def cache_key(api_key) - "lockbox_#{api_key}" - end - def auth_cache(api_key) - expiration = @cache.read(cache_key(api_key)) + def check_key_cache(api_key) + expiration = @cache.read(cache_string_for_key(api_key)) return nil if expiration.nil? expiration = Time.at(expiration) if expiration <= Time.now - @cache.delete(cache_key(api_key)) + @cache.delete(cache_string_for_key(api_key)) nil - elsif expiration > Time.now + else true end end - def cache_auth(api_key,expiration) - @cache.write(cache_key(api_key),expiration.to_i) + def check_hmac_cache(hmac_request) + hmac_id, hmac_hash = hmac_request.hmac_id, hmac_request.hmac_hash + return nil if hmac_id.nil? || hmac_hash.nil? + cached_val = @cache.read(cache_string_for_hmac(hmac_id)) + return nil if cached_val.nil? + key, expiration = cached_val + expiration = Time.at(expiration) + if expiration <= Time.now + @cache.delete(cache_string_for_hmac(hmac_id)) + nil + else + #as long as the request is signed correctly, no need to contact the lockbox server to verify + #just see if the request is signed properly and let it through if it is + return true if hmac_request.hmac_auth({hmac_id => key}) == key + return nil + end end - end