diff options
Diffstat (limited to 'actionpack')
-rw-r--r-- | actionpack/CHANGELOG | 2 | ||||
-rw-r--r-- | actionpack/actionpack.gemspec | 2 | ||||
-rw-r--r-- | actionpack/lib/action_controller/record_identifier.rb | 4 | ||||
-rw-r--r-- | actionpack/lib/action_controller/vendor/html-scanner/html/document.rb | 2 | ||||
-rw-r--r-- | actionpack/lib/action_dispatch.rb | 2 | ||||
-rw-r--r-- | actionpack/lib/action_dispatch/http/request.rb | 10 | ||||
-rw-r--r-- | actionpack/lib/action_dispatch/middleware/cookies.rb | 2 | ||||
-rw-r--r-- | actionpack/lib/action_dispatch/middleware/request_id.rb | 39 | ||||
-rw-r--r-- | actionpack/lib/action_dispatch/middleware/session/cache_store.rb | 50 | ||||
-rw-r--r-- | actionpack/test/controller/mime_responds_test.rb | 4 | ||||
-rw-r--r-- | actionpack/test/dispatch/request_id_test.rb | 65 | ||||
-rw-r--r-- | actionpack/test/dispatch/session/cache_store_test.rb | 181 | ||||
-rw-r--r-- | actionpack/test/template/html-scanner/tag_node_test.rb | 7 |
13 files changed, 363 insertions, 7 deletions
diff --git a/actionpack/CHANGELOG b/actionpack/CHANGELOG index 29992a36b1..e7886facb9 100644 --- a/actionpack/CHANGELOG +++ b/actionpack/CHANGELOG @@ -1,5 +1,7 @@ *Rails 3.2.0 (unreleased)* +* Added ActionDispatch::RequestId middleware that'll make a unique X-Request-Id header available to the response and enables the ActionDispatch::Request#uuid method. This makes it easy to trace requests from end-to-end in the stack and to identify individual requests in mixed logs like Syslog [DHH] + * Limit the number of options for select_year to 1000. Pass the :max_years_allowed option to set your own limit. diff --git a/actionpack/actionpack.gemspec b/actionpack/actionpack.gemspec index 78b9a4d5a8..c2eac234b0 100644 --- a/actionpack/actionpack.gemspec +++ b/actionpack/actionpack.gemspec @@ -24,7 +24,7 @@ Gem::Specification.new do |s| s.add_dependency('rack', '~> 1.3.5') s.add_dependency('rack-test', '~> 0.6.1') s.add_dependency('journey', '~> 1.0.0') - s.add_dependency('sprockets', '~> 2.0.2') + s.add_dependency('sprockets', '~> 2.0.3') s.add_dependency('erubis', '~> 2.7.0') s.add_development_dependency('tzinfo', '~> 0.3.29') diff --git a/actionpack/lib/action_controller/record_identifier.rb b/actionpack/lib/action_controller/record_identifier.rb index 2036442cfe..9c38ff44d8 100644 --- a/actionpack/lib/action_controller/record_identifier.rb +++ b/actionpack/lib/action_controller/record_identifier.rb @@ -14,9 +14,9 @@ module ActionController # <% end %> </div> # # # controller - # def destroy + # def update # post = Post.find(params[:id]) - # post.destroy + # post.update_attributes(params[:post]) # # redirect_to(post) # Calls polymorphic_url(post) which in turn calls post_url(post) # end diff --git a/actionpack/lib/action_controller/vendor/html-scanner/html/document.rb b/actionpack/lib/action_controller/vendor/html-scanner/html/document.rb index 7fa3aead82..386820300a 100644 --- a/actionpack/lib/action_controller/vendor/html-scanner/html/document.rb +++ b/actionpack/lib/action_controller/vendor/html-scanner/html/document.rb @@ -4,7 +4,7 @@ require 'html/selector' require 'html/sanitizer' module HTML #:nodoc: - # A top-level HTMl document. You give it a body of text, and it will parse that + # A top-level HTML document. You give it a body of text, and it will parse that # text into a tree of nodes. class Document #:nodoc: diff --git a/actionpack/lib/action_dispatch.rb b/actionpack/lib/action_dispatch.rb index 7f972fc281..1e92d14542 100644 --- a/actionpack/lib/action_dispatch.rb +++ b/actionpack/lib/action_dispatch.rb @@ -47,6 +47,7 @@ module ActionDispatch end autoload_under 'middleware' do + autoload :RequestId autoload :BestStandardsSupport autoload :Callbacks autoload :Cookies @@ -82,6 +83,7 @@ module ActionDispatch autoload :AbstractStore, 'action_dispatch/middleware/session/abstract_store' autoload :CookieStore, 'action_dispatch/middleware/session/cookie_store' autoload :MemCacheStore, 'action_dispatch/middleware/session/mem_cache_store' + autoload :CacheStore, 'action_dispatch/middleware/session/cache_store' end autoload_under 'testing' do diff --git a/actionpack/lib/action_dispatch/http/request.rb b/actionpack/lib/action_dispatch/http/request.rb index 37d0a3e0b8..7a5237dcf3 100644 --- a/actionpack/lib/action_dispatch/http/request.rb +++ b/actionpack/lib/action_dispatch/http/request.rb @@ -177,6 +177,16 @@ module ActionDispatch @remote_ip ||= (@env["action_dispatch.remote_ip"] || ip).to_s end + # Returns the unique request id, which is based off either the X-Request-Id header that can + # be generated by a firewall, load balancer, or web server or by the RequestId middleware + # (which sets the action_dispatch.request_id environment variable). + # + # This unique ID is useful for tracing a request from end-to-end as part of logging or debugging. + # This relies on the rack variable set by the ActionDispatch::RequestId middleware. + def uuid + @uuid ||= env["action_dispatch.request_id"] + end + # Returns the lowercase name of the HTTP server software. def server_software (@env['SERVER_SOFTWARE'] && /^([a-zA-Z]+)/ =~ @env['SERVER_SOFTWARE']) ? $1.downcase : nil diff --git a/actionpack/lib/action_dispatch/middleware/cookies.rb b/actionpack/lib/action_dispatch/middleware/cookies.rb index 8c4615c0c1..a4ffd40a66 100644 --- a/actionpack/lib/action_dispatch/middleware/cookies.rb +++ b/actionpack/lib/action_dispatch/middleware/cookies.rb @@ -174,7 +174,7 @@ module ActionDispatch options = { :value => value } end - value = @cookies[key.to_s] = value + @cookies[key.to_s] = value handle_options(options) diff --git a/actionpack/lib/action_dispatch/middleware/request_id.rb b/actionpack/lib/action_dispatch/middleware/request_id.rb new file mode 100644 index 0000000000..bee446c8a5 --- /dev/null +++ b/actionpack/lib/action_dispatch/middleware/request_id.rb @@ -0,0 +1,39 @@ +require 'securerandom' +require 'active_support/core_ext/string/access' +require 'active_support/core_ext/object/blank' + +module ActionDispatch + # Makes a unique request id available to the action_dispatch.request_id env variable (which is then accessible through + # ActionDispatch::Request#uuid) and sends the same id to the client via the X-Request-Id header. + # + # The unique request id is either based off the X-Request-Id header in the request, which would typically be generated + # by a firewall, load balancer, or the web server, or, if this header is not available, a random uuid. If the + # header is accepted from the outside world, we sanitize it to a max of 255 chars and alphanumeric and dashes only. + # + # The unique request id can be used to trace a request end-to-end and would typically end up being part of log files + # from multiple pieces of the stack. + class RequestId + def initialize(app) + @app = app + end + + def call(env) + env["action_dispatch.request_id"] = external_request_id(env) || internal_request_id + status, headers, body = @app.call(env) + + headers["X-Request-Id"] = env["action_dispatch.request_id"] + [ status, headers, body ] + end + + private + def external_request_id(env) + if request_id = env["HTTP_X_REQUEST_ID"].presence + request_id.gsub(/[^\w\-]/, "").first(255) + end + end + + def internal_request_id + SecureRandom.hex(16) + end + end +end diff --git a/actionpack/lib/action_dispatch/middleware/session/cache_store.rb b/actionpack/lib/action_dispatch/middleware/session/cache_store.rb new file mode 100644 index 0000000000..d3b6fd12fa --- /dev/null +++ b/actionpack/lib/action_dispatch/middleware/session/cache_store.rb @@ -0,0 +1,50 @@ +require 'action_dispatch/middleware/session/abstract_store' +require 'rack/session/memcache' + +module ActionDispatch + module Session + # Session store that uses an ActiveSupport::Cache::Store to store the sessions. This store is most useful + # if you don't store critical data in your sessions and you don't need them to live for extended periods + # of time. + class CacheStore < AbstractStore + # Create a new store. The cache to use can be passed in the <tt>:cache</tt> option. If it is + # not specified, <tt>Rails.cache</tt> will be used. + def initialize(app, options = {}) + @cache = options[:cache] || Rails.cache + options[:expire_after] ||= @cache.options[:expires_in] + super + end + + # Get a session from the cache. + def get_session(env, sid) + sid ||= generate_sid + session = @cache.read(cache_key(sid)) + session ||= {} + [sid, session] + end + + # Set a session in the cache. + def set_session(env, sid, session, options) + key = cache_key(sid) + if session + @cache.write(key, session, :expires_in => options[:expire_after]) + else + @cache.delete(key) + end + sid + end + + # Remove a session from the cache. + def destroy_session(env, sid, options) + @cache.delete(cache_key(sid)) + generate_sid + end + + private + # Turn the session id into a cache key. + def cache_key(sid) + "_session_id:#{sid}" + end + end + end +end diff --git a/actionpack/test/controller/mime_responds_test.rb b/actionpack/test/controller/mime_responds_test.rb index 2aa73f1650..e91e11a8a7 100644 --- a/actionpack/test/controller/mime_responds_test.rb +++ b/actionpack/test/controller/mime_responds_test.rb @@ -656,7 +656,9 @@ class RespondWithControllerTest < ActionController::TestCase @request.accept = "application/json" get :using_hash_resource assert_equal "application/json", @response.content_type - assert_equal %Q[{"result":{"name":"david","id":13}}], @response.body + assert @response.body.include?("result") + assert @response.body.include?('"name":"david"') + assert @response.body.include?('"id":13') end def test_using_hash_resource_with_post diff --git a/actionpack/test/dispatch/request_id_test.rb b/actionpack/test/dispatch/request_id_test.rb new file mode 100644 index 0000000000..b6e8b6c3ad --- /dev/null +++ b/actionpack/test/dispatch/request_id_test.rb @@ -0,0 +1,65 @@ +require 'abstract_unit' + +class RequestIdTest < ActiveSupport::TestCase + test "passing on the request id from the outside" do + assert_equal "external-uu-rid", stub_request('HTTP_X_REQUEST_ID' => 'external-uu-rid').uuid + end + + test "ensure that only alphanumeric uurids are accepted" do + assert_equal "X-Hacked-HeaderStuff", stub_request('HTTP_X_REQUEST_ID' => '; X-Hacked-Header: Stuff').uuid + end + + test "ensure that 255 char limit on the request id is being enforced" do + assert_equal "X" * 255, stub_request('HTTP_X_REQUEST_ID' => 'X' * 500).uuid + end + + test "generating a request id when none is supplied" do + assert_match(/\w+/, stub_request.uuid) + end + + private + + def stub_request(env = {}) + ActionDispatch::RequestId.new(lambda { |environment| [ 200, environment, [] ] }).call(env) + ActionDispatch::Request.new(env) + end +end + +class RequestIdResponseTest < ActionDispatch::IntegrationTest + class TestController < ActionController::Base + def index + head :ok + end + end + + test "request id is passed all the way to the response" do + with_test_route_set do + get '/' + assert_match(/\w+/, @response.headers["X-Request-Id"]) + end + end + + test "request id given on request is passed all the way to the response" do + with_test_route_set do + get '/', {}, 'HTTP_X_REQUEST_ID' => 'X' * 500 + assert_equal "X" * 255, @response.headers["X-Request-Id"] + end + end + + + private + + def with_test_route_set + with_routing do |set| + set.draw do + match '/', :to => ::RequestIdResponseTest::TestController.action(:index) + end + + @app = self.class.build_app(set) do |middleware| + middleware.use ActionDispatch::RequestId + end + + yield + end + end +end
\ No newline at end of file diff --git a/actionpack/test/dispatch/session/cache_store_test.rb b/actionpack/test/dispatch/session/cache_store_test.rb new file mode 100644 index 0000000000..73e056de23 --- /dev/null +++ b/actionpack/test/dispatch/session/cache_store_test.rb @@ -0,0 +1,181 @@ +require 'abstract_unit' + +class CacheStoreTest < ActionDispatch::IntegrationTest + class TestController < ActionController::Base + def no_session_access + head :ok + end + + def set_session_value + session[:foo] = "bar" + head :ok + end + + def set_serialized_session_value + session[:foo] = SessionAutoloadTest::Foo.new + head :ok + end + + def get_session_value + render :text => "foo: #{session[:foo].inspect}" + end + + def get_session_id + render :text => "#{request.session_options[:id]}" + end + + def call_reset_session + session[:bar] + reset_session + session[:bar] = "baz" + head :ok + end + + def rescue_action(e) raise end + end + + def test_setting_and_getting_session_value + with_test_route_set do + get '/set_session_value' + assert_response :success + assert cookies['_session_id'] + + get '/get_session_value' + assert_response :success + assert_equal 'foo: "bar"', response.body + end + end + + def test_getting_nil_session_value + with_test_route_set do + get '/get_session_value' + assert_response :success + assert_equal 'foo: nil', response.body + end + end + + def test_getting_session_value_after_session_reset + with_test_route_set do + get '/set_session_value' + assert_response :success + assert cookies['_session_id'] + session_cookie = cookies.send(:hash_for)['_session_id'] + + get '/call_reset_session' + assert_response :success + assert_not_equal [], headers['Set-Cookie'] + + cookies << session_cookie # replace our new session_id with our old, pre-reset session_id + + get '/get_session_value' + assert_response :success + assert_equal 'foo: nil', response.body, "data for this session should have been obliterated from cache" + end + end + + def test_getting_from_nonexistent_session + with_test_route_set do + get '/get_session_value' + assert_response :success + assert_equal 'foo: nil', response.body + assert_nil cookies['_session_id'], "should only create session on write, not read" + end + end + + def test_setting_session_value_after_session_reset + with_test_route_set do + get '/set_session_value' + assert_response :success + assert cookies['_session_id'] + session_id = cookies['_session_id'] + + get '/call_reset_session' + assert_response :success + assert_not_equal [], headers['Set-Cookie'] + + get '/get_session_value' + assert_response :success + assert_equal 'foo: nil', response.body + + get '/get_session_id' + assert_response :success + assert_not_equal session_id, response.body + end + end + + def test_getting_session_id + with_test_route_set do + get '/set_session_value' + assert_response :success + assert cookies['_session_id'] + session_id = cookies['_session_id'] + + get '/get_session_id' + assert_response :success + assert_equal session_id, response.body, "should be able to read session id without accessing the session hash" + end + end + + def test_deserializes_unloaded_class + with_test_route_set do + with_autoload_path "session_autoload_test" do + get '/set_serialized_session_value' + assert_response :success + assert cookies['_session_id'] + end + with_autoload_path "session_autoload_test" do + get '/get_session_id' + assert_response :success + end + with_autoload_path "session_autoload_test" do + get '/get_session_value' + assert_response :success + assert_equal 'foo: #<SessionAutoloadTest::Foo bar:"baz">', response.body, "should auto-load unloaded class" + end + end + end + + def test_doesnt_write_session_cookie_if_session_id_is_already_exists + with_test_route_set do + get '/set_session_value' + assert_response :success + assert cookies['_session_id'] + + get '/get_session_value' + assert_response :success + assert_equal nil, headers['Set-Cookie'], "should not resend the cookie again if session_id cookie is already exists" + end + end + + def test_prevents_session_fixation + with_test_route_set do + get '/get_session_value' + assert_response :success + assert_equal 'foo: nil', response.body + session_id = cookies['_session_id'] + + reset! + + get '/set_session_value', :_session_id => session_id + assert_response :success + assert_not_equal session_id, cookies['_session_id'] + end + end + + private + def with_test_route_set + with_routing do |set| + set.draw do + match ':action', :to => ::CacheStoreTest::TestController + end + + @app = self.class.build_app(set) do |middleware| + cache = ActiveSupport::Cache::MemoryStore.new + middleware.use ActionDispatch::Session::CacheStore, :key => '_session_id', :cache => cache + middleware.delete "ActionDispatch::ShowExceptions" + end + + yield + end + end +end diff --git a/actionpack/test/template/html-scanner/tag_node_test.rb b/actionpack/test/template/html-scanner/tag_node_test.rb index 0d87f1bd42..3b72243e7d 100644 --- a/actionpack/test/template/html-scanner/tag_node_test.rb +++ b/actionpack/test/template/html-scanner/tag_node_test.rb @@ -55,7 +55,12 @@ class TagNodeTest < Test::Unit::TestCase def test_to_s node = tag("<a b=c d='f' g=\"h 'i'\" />") - assert_equal %(<a b="c" d="f" g="h 'i'" />), node.to_s + node = node.to_s + assert node.include?('a') + assert node.include?('b="c"') + assert node.include?('d="f"') + assert node.include?('g="h') + assert node.include?('i') end def test_tag |