From 8826905ec6c4bb48cfc83245685099577e3215df Mon Sep 17 00:00:00 2001 From: David Massad Date: Tue, 18 Apr 2017 18:43:28 -0500 Subject: [PATCH 01/30] Changed rack version dependency so that it works with newer versions of rack. --- aws-sessionstore-dynamodb.gemspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aws-sessionstore-dynamodb.gemspec b/aws-sessionstore-dynamodb.gemspec index 8bf05cf..9c2b702 100644 --- a/aws-sessionstore-dynamodb.gemspec +++ b/aws-sessionstore-dynamodb.gemspec @@ -14,5 +14,5 @@ Gem::Specification.new do |spec| spec.require_paths = ["lib"] spec.add_dependency 'aws-sdk-v1' - spec.add_dependency 'rack', '~> 1.0' + spec.add_dependency 'rack', '>= 1.0' end From c3f2194a2ba7663051dff09d93a57bfe9a941a89 Mon Sep 17 00:00:00 2001 From: David Massad Date: Fri, 21 Apr 2017 11:15:59 -0500 Subject: [PATCH 02/30] Fixed issue where session item is found with no data. --- lib/aws/session_store/dynamo_db/locking/null.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/aws/session_store/dynamo_db/locking/null.rb b/lib/aws/session_store/dynamo_db/locking/null.rb index b7e5222..918e445 100644 --- a/lib/aws/session_store/dynamo_db/locking/null.rb +++ b/lib/aws/session_store/dynamo_db/locking/null.rb @@ -32,8 +32,8 @@ def get_session_opts(sid) # @return [String] Session data. def extract_data(env, result = nil) - env['rack.initial_data'] = result[:item]["data"][:s] if result[:item] - unpack_data(result[:item]["data"][:s]) if result[:item] + env['rack.initial_data'] = result[:item]["data"][:s] if result[:item] && result[:item]["data"] + unpack_data(result[:item]["data"][:s]) if result[:item] && result[:item]["data"] end end From 19e175c78dcfd2c4e5e03fca4efcc946ae8d2eae Mon Sep 17 00:00:00 2001 From: David Massad Date: Mon, 24 Apr 2017 11:17:00 -0500 Subject: [PATCH 03/30] Fixed issue where error occurs when deleting a session that does not exist. --- lib/aws/session_store/dynamo_db/locking/base.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/aws/session_store/dynamo_db/locking/base.rb b/lib/aws/session_store/dynamo_db/locking/base.rb index 1362756..6e82071 100644 --- a/lib/aws/session_store/dynamo_db/locking/base.rb +++ b/lib/aws/session_store/dynamo_db/locking/base.rb @@ -143,7 +143,7 @@ def data_unchanged?(env, session) # Expected attributes def expected_attributes(sid) - { :expected => {@config.table_key => {:value => {:s => sid}, :exists => true}} } + { :expected => {@config.table_key => {:value => {:s => sid}, :exists => false}} } end # Attributes to be retrieved via client From 4618999bae74a875faccbd2ff4efeace6bc959f9 Mon Sep 17 00:00:00 2001 From: David Massad Date: Tue, 25 Apr 2017 09:42:36 -0500 Subject: [PATCH 04/30] Merge pull request #13: Add expire_at attribute you can configure TTL on for garbage collection. --- lib/aws/session_store/dynamo_db/locking/base.rb | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/lib/aws/session_store/dynamo_db/locking/base.rb b/lib/aws/session_store/dynamo_db/locking/base.rb index 6e82071..c76e3f1 100644 --- a/lib/aws/session_store/dynamo_db/locking/base.rb +++ b/lib/aws/session_store/dynamo_db/locking/base.rb @@ -109,8 +109,8 @@ def table_opts(sid) def attr_updts(env, session, add_attrs = {}) data = data_unchanged?(env, session) ? {} : data_attr(session) { - :attribute_updates => merge_all(updated_attr, data, add_attrs), - :return_values => "UPDATED_NEW" + attribute_updates: merge_all(updated_attr, data, add_attrs, expire_attr), + return_values: 'UPDATED_NEW' } end @@ -124,6 +124,17 @@ def created_attr { "created_at" => updated_at } end + # Update client with current time + max_stale. + def expire_at + max_stale = @config.max_stale || 0 + { value: (Time.now + max_stale).to_i, action: 'PUT' } + end + + # Attribute for TTL expiration of session. + def expire_attr + { 'expire_at' => expire_at } + end + # Attribute for updating session. def updated_attr { From af9d8b33fb7d3175529a0066bf0f22e7600d6b62 Mon Sep 17 00:00:00 2001 From: David Massad Date: Tue, 25 Apr 2017 09:50:06 -0500 Subject: [PATCH 05/30] Changed value for exipre_at to a hash --- lib/aws/session_store/dynamo_db/locking/base.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/aws/session_store/dynamo_db/locking/base.rb b/lib/aws/session_store/dynamo_db/locking/base.rb index c76e3f1..48714cb 100644 --- a/lib/aws/session_store/dynamo_db/locking/base.rb +++ b/lib/aws/session_store/dynamo_db/locking/base.rb @@ -127,7 +127,7 @@ def created_attr # Update client with current time + max_stale. def expire_at max_stale = @config.max_stale || 0 - { value: (Time.now + max_stale).to_i, action: 'PUT' } + { value: { n: (Time.now + max_stale).to_i }, action: 'PUT' } end # Attribute for TTL expiration of session. From f0ecb0e4a6a0b59ff6e36c34ee61f42003b6e545 Mon Sep 17 00:00:00 2001 From: David Massad Date: Tue, 25 Apr 2017 09:51:57 -0500 Subject: [PATCH 06/30] Changed value of n to a string --- lib/aws/session_store/dynamo_db/locking/base.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/aws/session_store/dynamo_db/locking/base.rb b/lib/aws/session_store/dynamo_db/locking/base.rb index 48714cb..69f58c3 100644 --- a/lib/aws/session_store/dynamo_db/locking/base.rb +++ b/lib/aws/session_store/dynamo_db/locking/base.rb @@ -127,7 +127,7 @@ def created_attr # Update client with current time + max_stale. def expire_at max_stale = @config.max_stale || 0 - { value: { n: (Time.now + max_stale).to_i }, action: 'PUT' } + { value: { n: "#{(Time.now + max_stale).to_i }", action: 'PUT' } end # Attribute for TTL expiration of session. From 2dfa52a508a08d49140ef981d432c97f2c22a0b5 Mon Sep 17 00:00:00 2001 From: David Massad Date: Tue, 25 Apr 2017 09:52:46 -0500 Subject: [PATCH 07/30] Removed extra space --- lib/aws/session_store/dynamo_db/locking/base.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/aws/session_store/dynamo_db/locking/base.rb b/lib/aws/session_store/dynamo_db/locking/base.rb index 69f58c3..64a9282 100644 --- a/lib/aws/session_store/dynamo_db/locking/base.rb +++ b/lib/aws/session_store/dynamo_db/locking/base.rb @@ -127,7 +127,7 @@ def created_attr # Update client with current time + max_stale. def expire_at max_stale = @config.max_stale || 0 - { value: { n: "#{(Time.now + max_stale).to_i }", action: 'PUT' } + { value: { n: "#{(Time.now + max_stale).to_i}", action: 'PUT' } end # Attribute for TTL expiration of session. From 10cb346a79e28b8c528d1f0a64c2a165ae193f4c Mon Sep 17 00:00:00 2001 From: David Massad Date: Tue, 25 Apr 2017 10:32:27 -0500 Subject: [PATCH 08/30] Added missing brace. --- lib/aws/session_store/dynamo_db/locking/base.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/aws/session_store/dynamo_db/locking/base.rb b/lib/aws/session_store/dynamo_db/locking/base.rb index 64a9282..aa89181 100644 --- a/lib/aws/session_store/dynamo_db/locking/base.rb +++ b/lib/aws/session_store/dynamo_db/locking/base.rb @@ -127,7 +127,7 @@ def created_attr # Update client with current time + max_stale. def expire_at max_stale = @config.max_stale || 0 - { value: { n: "#{(Time.now + max_stale).to_i}", action: 'PUT' } + { value: { n: "#{(Time.now + max_stale).to_i}" }, action: 'PUT' } end # Attribute for TTL expiration of session. From 38cbc9e4031f509ce10fa4c8c2927c39fc7056f0 Mon Sep 17 00:00:00 2001 From: David Massad Date: Tue, 25 Apr 2017 14:09:26 -0500 Subject: [PATCH 09/30] Merged pull request #11: Remove expected attributes from deleting session. --- lib/aws/session_store/dynamo_db/locking/base.rb | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/lib/aws/session_store/dynamo_db/locking/base.rb b/lib/aws/session_store/dynamo_db/locking/base.rb index aa89181..f1943ce 100644 --- a/lib/aws/session_store/dynamo_db/locking/base.rb +++ b/lib/aws/session_store/dynamo_db/locking/base.rb @@ -65,7 +65,7 @@ def handle_error(env = nil, &block) # @return [Hash] Options for deleting session. def delete_opts(sid) - merge_all(table_opts(sid), expected_attributes(sid)) + table_opts(sid) end # @return [Hash] Options for updating item in Session table. @@ -152,11 +152,6 @@ def data_unchanged?(env, session) env['rack.initial_data'] == session end - # Expected attributes - def expected_attributes(sid) - { :expected => {@config.table_key => {:value => {:s => sid}, :exists => false}} } - end - # Attributes to be retrieved via client def attr_opts {:attributes_to_get => ["data"], From 8969995a268a07f794cf99e6d4603c6a0fdccf8a Mon Sep 17 00:00:00 2001 From: Jeff Dutil Date: Fri, 8 Sep 2017 19:31:58 -0400 Subject: [PATCH 10/30] Remove expected attributes from delete options. --- lib/aws/session_store/dynamo_db/locking/base.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/aws/session_store/dynamo_db/locking/base.rb b/lib/aws/session_store/dynamo_db/locking/base.rb index 8516f96..84e4adb 100644 --- a/lib/aws/session_store/dynamo_db/locking/base.rb +++ b/lib/aws/session_store/dynamo_db/locking/base.rb @@ -65,7 +65,7 @@ def handle_error(env = nil, &block) # @return [Hash] Options for deleting session. def delete_opts(sid) - merge_all(table_opts(sid), expected_attributes(sid)) + table_opts(sid) end # @return [Hash] Options for updating item in Session table. From 67f68e791e472f8a616318cbff56a08fe672116e Mon Sep 17 00:00:00 2001 From: David Massad Date: Mon, 2 Oct 2017 15:03:17 -0500 Subject: [PATCH 11/30] Updated expire_at method. --- lib/aws/session_store/dynamo_db/locking/base.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/aws/session_store/dynamo_db/locking/base.rb b/lib/aws/session_store/dynamo_db/locking/base.rb index 41c7219..f5013db 100644 --- a/lib/aws/session_store/dynamo_db/locking/base.rb +++ b/lib/aws/session_store/dynamo_db/locking/base.rb @@ -127,7 +127,7 @@ def created_attr # Update client with current time + max_stale. def expire_at max_stale = @config.max_stale || 0 - { value: { n: "#{(Time.now + max_stale).to_i}" }, action: 'PUT' } + { value: (Time.now + max_stale).to_i, action: 'PUT' } end # Attribute for TTL expiration of session. @@ -151,7 +151,7 @@ def data_unchanged?(env, session) return false unless env['rack.initial_data'] env['rack.initial_data'] == session end - + # Attributes to be retrieved via client def attr_opts {:attributes_to_get => ["data"], From 9fad25a5ffb48b17d1ce77939ed0a6789f6a75f0 Mon Sep 17 00:00:00 2001 From: ryokdy Date: Fri, 11 May 2018 23:59:29 +0900 Subject: [PATCH 12/30] Update aws-sdk version --- aws-sessionstore-dynamodb.gemspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aws-sessionstore-dynamodb.gemspec b/aws-sessionstore-dynamodb.gemspec index 83851b8..bba40f6 100644 --- a/aws-sessionstore-dynamodb.gemspec +++ b/aws-sessionstore-dynamodb.gemspec @@ -13,6 +13,6 @@ Gem::Specification.new do |spec| spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) spec.require_paths = ["lib"] - spec.add_dependency 'aws-sdk', '~> 2.0' + spec.add_dependency 'aws-sdk', '~> 3.0' spec.add_dependency 'rack', '>= 1.6.4' end From b807243705258d702165355ad5ee05da3d1f6840 Mon Sep 17 00:00:00 2001 From: ryokdy Date: Tue, 26 Jun 2018 12:48:30 +0900 Subject: [PATCH 13/30] Change attribute type of created_at and updated_at to float --- .../dynamo_db/garbage_collection.rb | 6 +-- .../session_store/dynamo_db/locking/base.rb | 2 +- .../dynamo_db/locking/pessimistic.rb | 4 +- .../dynamo_db/garbage_collection_spec.rb | 52 +++++++------------ 4 files changed, 26 insertions(+), 38 deletions(-) diff --git a/lib/aws/session_store/dynamo_db/garbage_collection.rb b/lib/aws/session_store/dynamo_db/garbage_collection.rb index 00458d1..f12404c 100644 --- a/lib/aws/session_store/dynamo_db/garbage_collection.rb +++ b/lib/aws/session_store/dynamo_db/garbage_collection.rb @@ -50,7 +50,7 @@ def scan_filter(config) # @api private def eliminate_unwanted_sessions(config, last_key = nil) scan_result = scan(config, last_key) - batch_delete(config, scan_result[:member]) + batch_delete(config, scan_result.items) scan_result[:last_evaluated_key] || {} end @@ -91,7 +91,7 @@ def process!(config, sub_batch) opts[:request_items] = {config.table_name => sub_batch} begin response = config.dynamo_db_client.batch_write_item(opts) - opts[:request_items] = response[:unprocessed_items] + opts[:request_items] = response.unprocessed_items end until opts[:request_items].empty? end @@ -114,7 +114,7 @@ def table_opts(config) # @api private def oldest_date(sec) hash = {} - hash[:attribute_value_list] = [:n => "#{((Time.now - sec).to_f)}"] + hash[:attribute_value_list] = [(Time.now - sec).to_f] hash[:comparison_operator] = 'LT' hash end diff --git a/lib/aws/session_store/dynamo_db/locking/base.rb b/lib/aws/session_store/dynamo_db/locking/base.rb index f5013db..a871f28 100644 --- a/lib/aws/session_store/dynamo_db/locking/base.rb +++ b/lib/aws/session_store/dynamo_db/locking/base.rb @@ -116,7 +116,7 @@ def attr_updts(env, session, add_attrs = {}) # Update client with current time attribute. def updated_at - { :value => "#{(Time.now).to_f}", :action => "PUT" } + { :value => (Time.now).to_f, :action => "PUT" } end # Attribute for creation of session. diff --git a/lib/aws/session_store/dynamo_db/locking/pessimistic.rb b/lib/aws/session_store/dynamo_db/locking/pessimistic.rb index 72831eb..61ed234 100644 --- a/lib/aws/session_store/dynamo_db/locking/pessimistic.rb +++ b/lib/aws/session_store/dynamo_db/locking/pessimistic.rb @@ -119,7 +119,7 @@ def lock_attr # Time in which session was updated. def updated_at - { :value => "#{(Time.now).to_f}", :action => "PUT" } + { :value => (Time.now).to_f, :action => "PUT" } end # Attributes for locking. @@ -147,7 +147,7 @@ def add_attr # Expectation of when lock was set. def expect_lock_time(env) { :expected => {"locked_at" => { - :value => "#{env["locked_at"]}", :exists => true}} } + :value => env["locked_at"], :exists => true}} } end # Attributes to be retrieved via client diff --git a/spec/aws/session_store/dynamo_db/garbage_collection_spec.rb b/spec/aws/session_store/dynamo_db/garbage_collection_spec.rb index f7071e0..7e2e429 100644 --- a/spec/aws/session_store/dynamo_db/garbage_collection_spec.rb +++ b/spec/aws/session_store/dynamo_db/garbage_collection_spec.rb @@ -17,7 +17,7 @@ def member(min,max) member = [] for i in min..max - member << {"session_id"=>{:s=>"#{i}"}} + member << {"session_id"=>"#{i}"} end member end @@ -25,7 +25,7 @@ def member(min,max) def format_scan_result member = [] for i in 31..49 - member << {"session_id"=>{:s=>"#{i}"}} + member << {"session_id"=>"#{i}"} end member.inject([]) do |rqst_array, item| @@ -40,61 +40,55 @@ def collect_garbage end let(:scan_resp1){ - resp = { - :member => member(0, 49), + Aws::DynamoDB::Types::ScanOutput.new({ + :items => member(0, 49), :count => 50, :scanned_count => 1000, :last_evaluated_key => {} - } + }) } let(:scan_resp2){ - { - :member => member(0, 31), - :last_evaluated_key => {"session_id"=>{:s=>"31"}} - } + Aws::DynamoDB::Types::ScanOutput.new({ + :items => member(0, 31), + :last_evaluated_key => {"session_id"=>"31"} + }) } let(:scan_resp3){ - { - :member => member(31,49), + Aws::DynamoDB::Types::ScanOutput.new({ + :items => member(31,49), :last_evaluated_key => {} - } + }) } let(:write_resp1){ - { + Aws::DynamoDB::Types::BatchWriteItemOutput.new({ :unprocessed_items => {} - } + }) } let(:write_resp2){ - { + Aws::DynamoDB::Types::BatchWriteItemOutput.new({ :unprocessed_items => { "sessions" => [ { :delete_request => { :key => { - "session_id" => - { - :s => "1" - } + "session_id" => "1" } } }, { :delete_request => { :key => { - "session_id" => - { - :s => "17" - } + "session_id" => "17" } } } ] } - } + }) } let(:dynamo_db_client) {Aws::DynamoDB::Client.new} @@ -133,20 +127,14 @@ def collect_garbage { :delete_request => { :key => { - "session_id" => - { - :s => "1" - } + "session_id" => "1" } } }, { :delete_request => { :key => { - "session_id" => - { - :s => "17" - } + "session_id" => "17" } } } From 5ecb1e0cd6895efa66757e5afa13cf264ddf2220 Mon Sep 17 00:00:00 2001 From: ryokdy Date: Tue, 26 Jun 2018 12:53:08 +0900 Subject: [PATCH 14/30] Make sure DynamoDB record has data attribute --- lib/aws/session_store/dynamo_db/locking/null.rb | 2 +- lib/aws/session_store/dynamo_db/locking/pessimistic.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/aws/session_store/dynamo_db/locking/null.rb b/lib/aws/session_store/dynamo_db/locking/null.rb index 869284e..b1210a3 100644 --- a/lib/aws/session_store/dynamo_db/locking/null.rb +++ b/lib/aws/session_store/dynamo_db/locking/null.rb @@ -33,7 +33,7 @@ def get_session_opts(sid) # @return [String] Session data. def extract_data(env, result = nil) env['rack.initial_data'] = result[:item]["data"] if result[:item] - unpack_data(result[:item]["data"]) if result[:item] + unpack_data(result[:item]["data"]) if result[:item] && result[:item].has_key?("data") end end diff --git a/lib/aws/session_store/dynamo_db/locking/pessimistic.rb b/lib/aws/session_store/dynamo_db/locking/pessimistic.rb index 61ed234..8c92d65 100644 --- a/lib/aws/session_store/dynamo_db/locking/pessimistic.rb +++ b/lib/aws/session_store/dynamo_db/locking/pessimistic.rb @@ -78,7 +78,7 @@ def get_data(env, result) lock_time = result[:attributes]["locked_at"] env["locked_at"] = (lock_time).to_f env['rack.initial_data'] = result[:item]["data"] if result.members.include? :item - unpack_data(result[:attributes]["data"]) + unpack_data(result[:attributes]["data"]) if result[:attributes].has_key?("data") end # Attempt to bust the lock if the expiration date has expired. From 018e580e2014a37cfcfe24677f83bff701e0cbd0 Mon Sep 17 00:00:00 2001 From: ryokdy Date: Wed, 31 May 2023 15:07:58 +0900 Subject: [PATCH 15/30] Remove .ruby-gemset and .ruby-version --- .ruby-gemset | 1 - .ruby-version | 1 - 2 files changed, 2 deletions(-) delete mode 100644 .ruby-gemset delete mode 100644 .ruby-version diff --git a/.ruby-gemset b/.ruby-gemset deleted file mode 100644 index a6cae96..0000000 --- a/.ruby-gemset +++ /dev/null @@ -1 +0,0 @@ -aws-sessionstore-dynamodb-ruby diff --git a/.ruby-version b/.ruby-version deleted file mode 100644 index 951d42f..0000000 --- a/.ruby-version +++ /dev/null @@ -1 +0,0 @@ -ruby-2.4.2 From df4b77741ad701689889107ce9e1fe74f3faac1e Mon Sep 17 00:00:00 2001 From: ryokdy Date: Tue, 7 Nov 2023 11:22:03 +0900 Subject: [PATCH 16/30] Upgrade rack to 3.0 --- aws-sessionstore-dynamodb.gemspec | 3 ++- lib/aws/session_store/dynamo_db/garbage_collection.rb | 4 ++-- spec/aws/session_store/dynamo_db/table_spec.rb | 8 +++++++- spec/spec_helper.rb | 8 +++++++- 4 files changed, 18 insertions(+), 5 deletions(-) diff --git a/aws-sessionstore-dynamodb.gemspec b/aws-sessionstore-dynamodb.gemspec index 6242fbc..e69bde1 100644 --- a/aws-sessionstore-dynamodb.gemspec +++ b/aws-sessionstore-dynamodb.gemspec @@ -16,5 +16,6 @@ Gem::Specification.new do |spec| spec.require_paths = ["lib"] spec.add_dependency 'aws-sdk-dynamodb', '~> 1' - spec.add_dependency 'rack', '~> 2' + spec.add_dependency 'rack', '~> 3' + spec.add_dependency 'rack-session', '>= 2.0.0' end diff --git a/lib/aws/session_store/dynamo_db/garbage_collection.rb b/lib/aws/session_store/dynamo_db/garbage_collection.rb index 10a1b30..f21d628 100644 --- a/lib/aws/session_store/dynamo_db/garbage_collection.rb +++ b/lib/aws/session_store/dynamo_db/garbage_collection.rb @@ -77,8 +77,8 @@ def process!(config, sub_batch) opts = {} opts[:request_items] = {config.table_name => sub_batch} begin - response = config.dynamo_db_client.batch_write_item(opts) - opts[:request_items] = response.unprocessed_items + response = config.dynamo_db_client.batch_write_item(**opts) + opts[:request_items] = response[:unprocessed_items] end until opts[:request_items].empty? end diff --git a/spec/aws/session_store/dynamo_db/table_spec.rb b/spec/aws/session_store/dynamo_db/table_spec.rb index 429ad63..7f83f26 100644 --- a/spec/aws/session_store/dynamo_db/table_spec.rb +++ b/spec/aws/session_store/dynamo_db/table_spec.rb @@ -23,7 +23,13 @@ module DynamoDB describe Table do context 'Mock Table Methods Tests', integration: true do let(:table_name) { "sessionstore-integration-test-#{Time.now.to_i}" } - let(:options) { { table_name: table_name } } + let(:dynamo_db_client) { + options = { + endpoint: 'http://localhost:8000' + } + Aws::DynamoDB::Client.new(options) + } + let(:options) { { table_name: table_name, dynamo_db_client: dynamo_db_client } } let(:io) { StringIO.new } before { allow(Table).to receive(:logger) { Logger.new(io) } } diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 44da4da..62e87d0 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -60,11 +60,17 @@ def call(env) RSpec.configure do |c| c.raise_errors_for_deprecations! c.before(:each, integration: true) do - opts = { table_name: 'sessionstore-integration-test' } + options = { + endpoint: + 'http://localhost:8000' + } + dynamo_db_client = Aws::DynamoDB::Client.new(options) + opts = { table_name: 'sessionstore-integration-test', dynamo_db_client: dynamo_db_client } defaults = Aws::SessionStore::DynamoDB::Configuration::DEFAULTS defaults = defaults.merge(opts) stub_const('Aws::SessionStore::DynamoDB::Configuration::DEFAULTS', defaults) + Aws::SessionStore::DynamoDB::Table.create_table(opts) end end From 9f6dafcfbd75d7f005aa4c376710a0326e150f99 Mon Sep 17 00:00:00 2001 From: ryokdy Date: Wed, 8 Nov 2023 13:36:30 +0900 Subject: [PATCH 17/30] Inherit ActionDispatch::Session::AbstractSecureStore instead of Rack::Session::Abstract::Persisted --- Gemfile | 1 + README.md | 28 ---- aws-sessionstore-dynamodb.gemspec | 1 + lib/aws-sessionstore-dynamodb.rb | 2 - .../session_store/dynamo_db/configuration.rb | 45 +----- .../dynamo_db/errors/default_handler.rb | 1 - .../session_store/dynamo_db/locking/base.rb | 128 +++++++--------- .../session_store/dynamo_db/locking/null.rb | 17 ++- .../dynamo_db/locking/pessimistic.rb | 144 ------------------ .../dynamo_db/missing_secret_key_error.rb | 7 - .../dynamo_db/rack_middleware.rb | 101 ++---------- .../error/default_error_handler_spec.rb | 18 +-- .../locking/threaded_sessions_spec.rb | 96 ------------ .../rack_middleware_database_spec.rb | 33 +--- .../dynamo_db/rack_middleware_spec.rb | 16 +- spec/spec_helper.rb | 33 +++- 16 files changed, 134 insertions(+), 537 deletions(-) delete mode 100644 lib/aws/session_store/dynamo_db/locking/pessimistic.rb delete mode 100644 lib/aws/session_store/dynamo_db/missing_secret_key_error.rb delete mode 100644 spec/aws/session_store/dynamo_db/locking/threaded_sessions_spec.rb diff --git a/Gemfile b/Gemfile index f9f3b34..d3b70c2 100644 --- a/Gemfile +++ b/Gemfile @@ -10,6 +10,7 @@ group :docs do end group :test do + gem 'debug' gem 'rspec' gem 'simplecov', require: false gem 'rack-test' diff --git a/README.md b/README.md index b09703d..f0ea3a5 100644 --- a/README.md +++ b/README.md @@ -84,34 +84,6 @@ garbage collection similar to below: The above example will clear sessions older than one day or that have been stale for longer than an hour. -### Locking Strategy - -You may want the Session Store to implement the provided pessimistic locking -strategy if you are concerned about concurrency issues with session accesses. -By default, locking is not implemented for the session store. You must trigger -the locking strategy through the configuration of the session store. Pessimistic -locking, in this case, means that only one read can be made on a session at -once. While the session is being read by the process with the lock, other -processes may try to obtain a lock on the same session but will be blocked. - -Locking is expensive and will drive up costs depending on how it is used. -Without locking, one read and one write are performed per request for session -data manipulation. If a locking strategy is implemented, as many as the total -maximum wait time divided by the lock retry delay writes to the database. -Keep these considerations in mind if you plan to enable locking. - -#### Configuration for Locking - -The following configuration options will allow you to configure the pessimistic -locking strategy according to your needs: - - options = { - :enable_locking => true, - :lock_expiry_time => 500, - :lock_retry_delay => 500, - :lock_max_wait_time => 1 - } - ### Error Handling You can pass in your own error handler for raised exceptions or you can allow diff --git a/aws-sessionstore-dynamodb.gemspec b/aws-sessionstore-dynamodb.gemspec index e69bde1..a5c0e4b 100644 --- a/aws-sessionstore-dynamodb.gemspec +++ b/aws-sessionstore-dynamodb.gemspec @@ -16,6 +16,7 @@ Gem::Specification.new do |spec| spec.require_paths = ["lib"] spec.add_dependency 'aws-sdk-dynamodb', '~> 1' + spec.add_dependency('actionpack', '>= 6.1') spec.add_dependency 'rack', '~> 3' spec.add_dependency 'rack-session', '>= 2.0.0' end diff --git a/lib/aws-sessionstore-dynamodb.rb b/lib/aws-sessionstore-dynamodb.rb index 4fa603b..3801075 100644 --- a/lib/aws-sessionstore-dynamodb.rb +++ b/lib/aws-sessionstore-dynamodb.rb @@ -6,14 +6,12 @@ module DynamoDB; end require 'aws/session_store/dynamo_db/configuration' require 'aws/session_store/dynamo_db/invalid_id_error' -require 'aws/session_store/dynamo_db/missing_secret_key_error' require 'aws/session_store/dynamo_db/lock_wait_timeout_error' require 'aws/session_store/dynamo_db/errors/base_handler' require 'aws/session_store/dynamo_db/errors/default_handler' require 'aws/session_store/dynamo_db/garbage_collection' require 'aws/session_store/dynamo_db/locking/base' require 'aws/session_store/dynamo_db/locking/null' -require 'aws/session_store/dynamo_db/locking/pessimistic' require 'aws/session_store/dynamo_db/rack_middleware' require 'aws/session_store/dynamo_db/table' require 'aws/session_store/dynamo_db/version' diff --git a/lib/aws/session_store/dynamo_db/configuration.rb b/lib/aws/session_store/dynamo_db/configuration.rb index 0cd37df..ed741a4 100644 --- a/lib/aws/session_store/dynamo_db/configuration.rb +++ b/lib/aws/session_store/dynamo_db/configuration.rb @@ -23,14 +23,6 @@ module Aws::SessionStore::DynamoDB # :error_handler as a cofniguration object. You must implement the BaseErrorHandler class. # @see BaseHandler Interface for Error Handling for DynamoDB Session Store. # - # == Locking Strategy - # By default, locking is not implemented for the session store. You must trigger the - # locking strategy through the configuration of the session store. Pessimistic locking, - # in this case, means that only one read can be made on a session at once. While the session - # is being read by the process with the lock, other processes may try to obtain a lock on - # the same session but will be blocked. See the accessors with lock in their name for - # how to configure the pessimistic locking strategy to your needs. - # # == DynamoDB Specific Options # You may configure the table name and table hash key value of your session table with # the :table_name and :table_key options. You may also configure performance options for @@ -52,8 +44,7 @@ class Configuration :enable_locking => false, :lock_expiry_time => 500, :lock_retry_delay => 500, - :lock_max_wait_time => 1, - :secret_key => nil + :lock_max_wait_time => 1 } ### Feature options @@ -94,26 +85,6 @@ class Configuration # before the current time that the session was last accessed. attr_reader :max_stale - # @return [true] Pessimistic locking strategy will be implemented for - # all session accesses. - # @return [false] No locking strategy will be implemented for - # all session accesses. - attr_reader :enable_locking - - # @return [Integer] Time in milleseconds after which lock will expire. - attr_reader :lock_expiry_time - - # @return [Integer] Time in milleseconds to wait before retrying to obtain - # lock once an attempt to obtain lock has been made and has failed. - attr_reader :lock_retry_delay - - # @return [Integer] Maximum time in seconds to wait to acquire lock - # before giving up. - attr_reader :lock_max_wait_time - - # @return [String] The secret key for HMAC encryption. - attr_reader :secret_key - # @return [String,Pathname] attr_reader :config_file @@ -159,20 +130,6 @@ class Configuration # from the current time that a session was created. # @option options [Integer] :max_stale (nil) Maximum number of seconds # before current time that session was last accessed. - # @option options [String] :secret_key (nil) Secret key for HMAC encription. - # @option options [Integer] :enable_locking (false) If true, a pessimistic - # locking strategy will be implemented for all session accesses. - # If false, no locking strategy will be implemented for all session - # accesses. - # @option options [Integer] :lock_expiry_time (500) Time in milliseconds - # after which lock expires on session. - # @option options [Integer] :lock_retry_delay (500) Time in milleseconds to - # wait before retrying to obtain lock once an attempt to obtain lock - # has been made and has failed. - # @option options [Integer] :lock_max_wait_time (500) Maximum time - # in seconds to wait to acquire lock before giving up. - # @option options [String] :secret_key (SecureRandom.hex(64)) - # Secret key for HMAC encription. def initialize(options = {}) @options = default_options.merge( env_options.merge( diff --git a/lib/aws/session_store/dynamo_db/errors/default_handler.rb b/lib/aws/session_store/dynamo_db/errors/default_handler.rb index 53e70a2..2e9fbfe 100644 --- a/lib/aws/session_store/dynamo_db/errors/default_handler.rb +++ b/lib/aws/session_store/dynamo_db/errors/default_handler.rb @@ -5,7 +5,6 @@ class DefaultHandler < Aws::SessionStore::DynamoDB::Errors::BaseHandler HARD_ERRORS = [ Aws::DynamoDB::Errors::ResourceNotFoundException, Aws::DynamoDB::Errors::ConditionalCheckFailedException, - Aws::SessionStore::DynamoDB::MissingSecretKeyError, Aws::SessionStore::DynamoDB::LockWaitTimeoutError ] diff --git a/lib/aws/session_store/dynamo_db/locking/base.rb b/lib/aws/session_store/dynamo_db/locking/base.rb index caeb699..02c7705 100644 --- a/lib/aws/session_store/dynamo_db/locking/base.rb +++ b/lib/aws/session_store/dynamo_db/locking/base.rb @@ -13,8 +13,14 @@ def set_session_data(env, sid, session, options = {}) return false if session.empty? packed_session = pack_data(session) handle_error(env) do - save_opts = update_opts(env, sid, packed_session, options) - @config.dynamo_db_client.update_item(save_opts) + if env['dynamo_db.new_session'] + save_options = save_new_opts(env, sid, packed_session) + @config.dynamo_db_client.put_item(save_options) + env.delete('dynamo_db.new_session') + else + save_options = save_exists_opts(env, sid, packed_session, options) + @config.dynamo_db_client.update_item(save_options) + end sid end end @@ -51,31 +57,58 @@ def handle_error(env = nil, &block) # @return [Hash] Options for deleting session. def delete_opts(sid) - table_opts(sid) - end - - # @return [Hash] Options for updating item in Session table. - def update_opts(env, sid, session, options = {}) - if env['dynamo_db.new_session'] - updt_options = save_new_opts(env, sid, session) - else - updt_options = save_exists_opts(env, sid, session, options) - end - updt_options + { + table_name: @config.table_name, + key: { + @config.table_key => sid + } + } end # @return [Hash] Options for saving a new session in database. def save_new_opts(env, sid, session) - attribute_opts = attr_updts(env, session, created_attr) - merge_all(table_opts(sid), attribute_opts) + { + table_name: @config.table_name, + item: { + @config.table_key => sid, + data: session.to_s, + created_at: created_at, + updated_at: updated_at, + expire_at: expire_at + }, + condition_expression: "attribute_not_exists(#{@config.table_key})" + } end # @return [Hash] Options for saving an existing sesison in the database. def save_exists_opts(env, sid, session, options = {}) - add_attr = options[:add_attrs] || {} - expected = options[:expect_attr] || {} - attribute_opts = merge_all(attr_updts(env, session, add_attr), expected) - merge_all(table_opts(sid), attribute_opts) + data = if data_unchanged?(env, session) + {} + else + { + data: { + value: session.to_s, + action: 'PUT' + } + } + end + { + table_name: @config.table_name, + key: { + @config.table_key => sid + }, + attribute_updates: { + updated_at: { + value: updated_at, + action: 'PUT' + }, + expire_at: { + value: expire_at, + action: 'PUT' + } + }.merge(data), + return_values: 'UPDATED_NEW' + } end # Unmarshal the data. @@ -83,53 +116,17 @@ def unpack_data(packed_data) Marshal.load(packed_data.unpack("m*").first) end - # Table options for client. - def table_opts(sid) - { - :table_name => @config.table_name, - :key => { @config.table_key => sid } - } - end - - # Attributes to update via client. - def attr_updts(env, session, add_attrs = {}) - data = data_unchanged?(env, session) ? {} : data_attr(session) - { - attribute_updates: merge_all(updated_attr, data, add_attrs, expire_attr), - return_values: 'UPDATED_NEW' - } - end - - # Update client with current time attribute. def updated_at - { :value => (Time.now).to_f, :action => "PUT" } + Time.now.to_f end - # Attribute for creation of session. - def created_attr - { "created_at" => updated_at } + def created_at + updated_at end - # Update client with current time + max_stale. def expire_at max_stale = @config.max_stale || 0 - { value: (Time.now + max_stale).to_i, action: 'PUT' } - end - - # Attribute for TTL expiration of session. - def expire_attr - { 'expire_at' => expire_at } - end - - # Attribute for updating session. - def updated_attr - { - "updated_at" => updated_at - } - end - - def data_attr(session) - { "data" => {:value => session, :action => "PUT"} } + (Time.now + max_stale).to_i end # Determine if data has been manipulated @@ -137,18 +134,5 @@ def data_unchanged?(env, session) return false unless env['rack.initial_data'] env['rack.initial_data'] == session end - - # Attributes to be retrieved via client - def attr_opts - {:attributes_to_get => ["data"], - :consistent_read => @config.consistent_read} - end - - # @return [Hash] merged hash of all hashes passed in. - def merge_all(*hashes) - new_hash = {} - hashes.each{|hash| new_hash.merge!(hash)} - new_hash - end end end diff --git a/lib/aws/session_store/dynamo_db/locking/null.rb b/lib/aws/session_store/dynamo_db/locking/null.rb index 28fe4e6..f9e711f 100644 --- a/lib/aws/session_store/dynamo_db/locking/null.rb +++ b/lib/aws/session_store/dynamo_db/locking/null.rb @@ -13,13 +13,24 @@ def get_session_data(env, sid) # @return [Hash] Options for getting session. def get_session_opts(sid) - merge_all(table_opts(sid), attr_opts) + { + table_name: @config.table_name, + key: { + @config.table_key => sid + }, + attributes_to_get: [ "data" ], + consistent_read: @config.consistent_read + } end # @return [String] Session data. def extract_data(env, result = nil) - env['rack.initial_data'] = result[:item]["data"] if result[:item] - unpack_data(result[:item]["data"]) if result[:item] && result[:item].has_key?("data") + if result[:item] && result[:item].has_key?("data") + env['rack.initial_data'] = result[:item]["data"] + unpack_data(result[:item]["data"]) + else + nil + end end end diff --git a/lib/aws/session_store/dynamo_db/locking/pessimistic.rb b/lib/aws/session_store/dynamo_db/locking/pessimistic.rb deleted file mode 100644 index 2a7b434..0000000 --- a/lib/aws/session_store/dynamo_db/locking/pessimistic.rb +++ /dev/null @@ -1,144 +0,0 @@ -module Aws::SessionStore::DynamoDB::Locking - # This class implements a pessimistic locking strategy for the - # DynamoDB session handler. Sessions obtain an exclusive lock - # for reads that is only released when the session is saved. - class Pessimistic < Aws::SessionStore::DynamoDB::Locking::Base - # Saves the session. - def set_session_data(env, sid, session, options = {}) - super(env, sid, session, set_lock_options(env, options)) - end - - # Gets session from database and places a lock on the session - # while you are reading from the database. - def get_session_data(env, sid) - handle_error(env) do - get_session_with_lock(env, sid) - end - end - - private - - # Get session with implemented locking strategy. - def get_session_with_lock(env, sid) - expires_at = nil - result = nil - max_attempt_date = Time.now.to_f + @config.lock_max_wait_time - while result.nil? - exceeded_wait_time?(max_attempt_date) - begin - result = attempt_set_lock(sid) - rescue Aws::DynamoDB::Errors::ConditionalCheckFailedException - expires_at ||= get_expire_date(sid) - next if expires_at.nil? - result = bust_lock(sid, expires_at) - wait_to_retry(result) - end - end - get_data(env, result) - end - - # Determine if session has waited too long to obtain lock. - # - # @raise [Error] When time for attempting to get lock has - # been exceeded. - def exceeded_wait_time?(max_attempt_date) - lock_error = Aws::SessionStore::DynamoDB::LockWaitTimeoutError - raise lock_error if Time.now.to_f > max_attempt_date - end - - # @return [Hash] Options hash for placing a lock on a session. - def get_lock_time_opts(sid) - merge_all(table_opts(sid), lock_opts) - end - - # @return [Time] Time stamp for which the session was locked. - def lock_time(sid) - result = @config.dynamo_db_client.get_item(get_lock_time_opts(sid)) - (result[:item]["locked_at"]).to_f if result[:item]["locked_at"] - end - - # @return [String] Session data. - def get_data(env, result) - lock_time = result[:attributes]["locked_at"] - env["locked_at"] = (lock_time).to_f - env['rack.initial_data'] = result[:item]["data"] if result.members.include? :item - unpack_data(result[:attributes]["data"]) if result[:attributes].has_key?("data") - end - - # Attempt to bust the lock if the expiration date has expired. - def bust_lock(sid, expires_at) - if expires_at < Time.now.to_f - @config.dynamo_db_client.update_item(obtain_lock_opts(sid)) - end - end - - # @return [Hash] Options hash for obtaining the lock. - def obtain_lock_opts(sid, add_opt = {}) - merge_all(table_opts(sid), lock_attr, add_opt) - end - - # Sleep for given time period if the session is currently locked. - def wait_to_retry(result) - sleep(0.001 * @config.lock_retry_delay) if result.nil? - end - - # Get the expiration date for the session - def get_expire_date(sid) - lock_date = lock_time(sid) - lock_date + (0.001 * @config.lock_expiry_time) if lock_date - end - - # Attempt to place a lock on the session. - def attempt_set_lock(sid) - @config.dynamo_db_client.update_item(obtain_lock_opts(sid, lock_expect)) - end - - # Lock attribute - time stamp of when session was locked. - def lock_attr - { - :attribute_updates => {"locked_at" => updated_at}, - :return_values => "ALL_NEW" - } - end - - # Time in which session was updated. - def updated_at - { :value => (Time.now).to_f, :action => "PUT" } - end - - # Attributes for locking. - def add_lock_attrs(env) - { - :add_attrs => add_attr, :expect_attr => expect_lock_time(env) - } - end - - # Lock options for setting lock. - def set_lock_options(env, options = {}) - merge_all(options, add_lock_attrs(env)) - end - - # Lock expectation. - def lock_expect - { :expected => { "locked_at" => { :exists => false } } } - end - - # Option to delete lock. - def add_attr - { "locked_at" => {:action => "DELETE"} } - end - - # Expectation of when lock was set. - def expect_lock_time(env) - { :expected => {"locked_at" => { - :value => env["locked_at"], :exists => true}} } - end - - # Attributes to be retrieved via client - def lock_opts - {:attributes_to_get => ["locked_at"], - :consistent_read => @config.consistent_read} - end - - end -end diff --git a/lib/aws/session_store/dynamo_db/missing_secret_key_error.rb b/lib/aws/session_store/dynamo_db/missing_secret_key_error.rb deleted file mode 100644 index ed13be0..0000000 --- a/lib/aws/session_store/dynamo_db/missing_secret_key_error.rb +++ /dev/null @@ -1,7 +0,0 @@ -module Aws::SessionStore::DynamoDB - class MissingSecretKeyError < RuntimeError - def initialize(msg = "No secret key provided!") - super - end - end -end diff --git a/lib/aws/session_store/dynamo_db/rack_middleware.rb b/lib/aws/session_store/dynamo_db/rack_middleware.rb index bad6a40..ec63939 100644 --- a/lib/aws/session_store/dynamo_db/rack_middleware.rb +++ b/lib/aws/session_store/dynamo_db/rack_middleware.rb @@ -1,23 +1,11 @@ -require 'rack/session/abstract/id' +require 'action_dispatch/middleware/session/abstract_store' require 'openssl' require 'aws-sdk-dynamodb' module Aws::SessionStore::DynamoDB - # This class is an ID based Session Store Rack Middleware - # that uses a DynamoDB backend for session storage. - class RackMiddleware < Rack::Session::Abstract::Persisted - # @return [Configuration] An instance of Configuration that is used for - # this middleware. + class RackMiddleware < ActionDispatch::Session::AbstractSecureStore attr_reader :config - # Initializes SessionStore middleware. - # - # @param app Rack application. - # @option (see Configuration#initialize) - # @raise [Aws::DynamoDB::Errors::ResourceNotFoundException] If valid table - # name is not provided. - # @raise [Aws::SessionStore::DynamoDB::MissingSecretKey] If secret key is - # not provided. def initialize(app, options = {}) super @config = Configuration.new(options) @@ -26,95 +14,36 @@ def initialize(app, options = {}) private - # Sets locking strategy for session handler - # - # @return [Locking::Null] If locking is not enabled. - # @return [Locking::Pessimistic] If locking is enabled. def set_locking_strategy - if @config.enable_locking - @lock = Aws::SessionStore::DynamoDB::Locking::Pessimistic.new(@config) - else - @lock = Aws::SessionStore::DynamoDB::Locking::Null.new(@config) - end - end - - # Determines if the correct session table name is being used for - # this application. Also tests existence of secret key. - # - # @raise [Aws::DynamoDB::Errors::ResourceNotFoundException] If wrong table - # name. - def validate_config - raise MissingSecretKeyError unless @config.secret_key + @lock = Aws::SessionStore::DynamoDB::Locking::Null.new(@config) end # Gets session data. def find_session(req, sid) - validate_config - case verify_hmac(sid) - when nil - set_new_session_properties(req.env) - when false - handle_error { raise InvalidIDError } - set_new_session_properties(req.env) + if req.session.options[:skip] + [generate_sid, {}] else - data = @lock.get_session_data(req.env, sid) - [sid, data || {}] + unless sid and session = @lock.get_session_data(req.env, sid.private_id) + session = {} + sid = generate_unique_sid(req.env, session) + end + [sid, session] end end - def set_new_session_properties(env) + def generate_unique_sid(env, session) env['dynamo_db.new_session'] = 'true' - [generate_sid, {}] + generate_sid end - # Sets the session in the database after packing data. - # - # @return [Hash] If session has been saved. - # @return [false] If session has could not be saved. def write_session(req, sid, session, options) - @lock.set_session_data(req.env, sid, session, options) + @lock.set_session_data(req.env, sid.private_id, session, options) + sid end - # Destroys session and removes session from database. - # - # @return [String] return a new session id or nil if options[:drop] def delete_session(req, sid, options) - @lock.delete_session(req.env, sid) + @lock.delete_session(req.env, sid.private_id) generate_sid unless options[:drop] end - - # Each database operation is placed in this rescue wrapper. - # This wrapper will call the method, rescue any exceptions and then pass - # exceptions to the configured session handler. - def handle_error(env = nil, &block) - begin - yield - rescue Aws::DynamoDB::Errors::Base, - Aws::SessionStore::DynamoDB::InvalidIDError => e - @config.error_handler.handle_error(e, env) - end - end - - # Generate HMAC hash based on MD5 - def generate_hmac(sid, secret) - OpenSSL::HMAC.hexdigest(OpenSSL::Digest::MD5.new, secret, sid).strip() - end - - # Generate sid with HMAC hash - def generate_sid(secure = @sid_secure) - sid = super(secure) - sid = "#{generate_hmac(sid, @config.secret_key)}--" + sid - end - - # Verify digest of HMACed hash - # - # @return [true] If the HMAC id has been verified. - # @return [false] If the HMAC id has been corrupted. - def verify_hmac(sid) - return unless sid - digest, ver_sid = sid.split("--") - return false unless ver_sid - digest == generate_hmac(ver_sid, @config.secret_key) - end end end diff --git a/spec/aws/session_store/dynamo_db/error/default_error_handler_spec.rb b/spec/aws/session_store/dynamo_db/error/default_error_handler_spec.rb index 4b8b3c3..f4ebad5 100644 --- a/spec/aws/session_store/dynamo_db/error/default_error_handler_spec.rb +++ b/spec/aws/session_store/dynamo_db/error/default_error_handler_spec.rb @@ -21,26 +21,20 @@ instance_exec(&ConstantHelpers) before do - @options = { dynamo_db_client: client, secret_key: 'meltingbutter' } + @options = { dynamo_db_client: client } end - let(:base_app) { MultiplierApplication.new } - let(:app) { Aws::SessionStore::DynamoDB::RackMiddleware.new(base_app, @options) } + let(:app) { RoutedRackApp.build(@options) } let(:client) { double('Aws::DynamoDB::Client') } context 'Error handling for Rack Middleware with default error handler' do - it 'raises error for missing secret key' do - allow(client).to receive(:update_item).and_raise(missing_key_error) - expect { get '/' }.to raise_error(missing_key_error) - end - it 'catches exception for inaccurate table name and raises error ' do - allow(client).to receive(:update_item).and_raise(resource_error) + allow(client).to receive(:put_item).and_raise(resource_error) expect { get '/' }.to raise_error(resource_error) end it 'catches exception for inaccurate table key' do - allow(client).to receive(:update_item).and_raise(key_error) + allow(client).to receive(:put_item).and_raise(key_error) allow(client).to receive(:get_item).and_raise(key_error) get '/' @@ -51,13 +45,13 @@ context 'Test ExceptionHandler with true as return value for handle_error' do it 'raises all errors' do @options[:raise_errors] = true - allow(client).to receive(:update_item).and_raise(client_error) + allow(client).to receive(:put_item).and_raise(client_error) expect { get '/' }.to raise_error(client_error) end it 'catches exception for inaccurate table key and raises error' do @options[:raise_errors] = true - allow(client).to receive(:update_item).and_raise(key_error) + allow(client).to receive(:put_item).and_raise(key_error) expect { get '/' }.to raise_error(key_error) end end diff --git a/spec/aws/session_store/dynamo_db/locking/threaded_sessions_spec.rb b/spec/aws/session_store/dynamo_db/locking/threaded_sessions_spec.rb deleted file mode 100644 index bdd7c5e..0000000 --- a/spec/aws/session_store/dynamo_db/locking/threaded_sessions_spec.rb +++ /dev/null @@ -1,96 +0,0 @@ -# frozen_string_literal: true - -# Copyright 2013 Amazon.com, Inc. or its affiliates. All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"). You -# may not use this file except in compliance with the License. A copy of -# the License is located at -# -# http://aws.amazon.com/apache2.0/ -# -# or in the "license" file accompanying this file. This file is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF -# ANY KIND, either express or implied. See the License for the specific -# language governing permissions and limitations under the License. - -require 'spec_helper' - -describe Aws::SessionStore::DynamoDB::RackMiddleware do - include Rack::Test::Methods - - def thread(mul_val, time, check) - Thread.new do - sleep(time) - get '/' - expect(last_request.session[:multiplier]).to eq(mul_val) if check - end - end - - def thread_exception(error) - Thread.new { expect { get '/' }.to raise_error(error) } - end - - def update_item_mock(options, update_method) - if options[:return_values] == 'UPDATED_NEW' && options.key?(:expected) - sleep(0.50) - update_method.call(options) - else - update_method.call(options) - end - end - - let(:base_app) { MultiplierApplication.new } - let(:app) { Aws::SessionStore::DynamoDB::RackMiddleware.new(base_app, @options) } - - context 'Mock Multiple Threaded Sessions', integration: true do - before do - @options = Aws::SessionStore::DynamoDB::Configuration.new.to_hash - @options[:enable_locking] = true - @options[:secret_key] = 'watermelon_smiles' - - update_method = @options[:dynamo_db_client].method(:update_item) - expect(@options[:dynamo_db_client]).to receive(:update_item).at_least(:once) do |options| - update_item_mock(options, update_method) - end - end - - it 'should wait for lock' do - @options[:lock_expiry_time] = 2000 - - get '/' - expect(last_request.session[:multiplier]).to eq(1) - - t1 = thread(2, 0, false) - t2 = thread(4, 0.25, true) - t1.join - t2.join - end - - it 'should bust lock' do - @options[:lock_expiry_time] = 100 - - get '/' - expect(last_request.session[:multiplier]).to eq(1) - - t1 = thread_exception(Aws::DynamoDB::Errors::ConditionalCheckFailedException) - t2 = thread(2, 0.25, true) - t1.join - t2.join - end - - it 'should throw exceeded time spent aquiring lock error' do - @options[:lock_expiry_time] = 1000 - @options[:lock_retry_delay] = 100 - @options[:lock_max_wait_time] = 0.25 - - get '/' - expect(last_request.session[:multiplier]).to eq(1) - - t1 = thread(2, 0, false) - sleep(0.25) - t2 = thread_exception(Aws::SessionStore::DynamoDB::LockWaitTimeoutError) - t1.join - t2.join - end - end -end diff --git a/spec/aws/session_store/dynamo_db/rack_middleware_database_spec.rb b/spec/aws/session_store/dynamo_db/rack_middleware_database_spec.rb index 6737c60..df3c61c 100644 --- a/spec/aws/session_store/dynamo_db/rack_middleware_database_spec.rb +++ b/spec/aws/session_store/dynamo_db/rack_middleware_database_spec.rb @@ -24,7 +24,7 @@ module DynamoDB instance_exec(&ConstantHelpers) before do - @options = { secret_key: 'watermelon_cherries' } + @options = {} end # Table options for client @@ -48,8 +48,7 @@ def extract_time(sid) Time.at((client.get_item(options)[:item]['created_at']).to_f) end - let(:base_app) { MultiplierApplication.new } - let(:app) { RackMiddleware.new(base_app, @options) } + let(:app) { RoutedRackApp.build(@options) } let(:config) { Configuration.new } let(:client) { config.dynamo_db_client } @@ -79,12 +78,6 @@ def extract_time(sid) expect(last_response['Set-Cookie']).to be_nil end - it 'creates new session with false/nonexistant http-cookie id' do - get '/', {}, invalid_cookie.merge(invalid_session_data) - expect(last_response['Set-Cookie']).not_to eq('rack.session=ApplePieBlueberries') - expect(last_response['Set-Cookie']).not_to be_nil - end - it 'expires after specified time and sets date for cookie to expire' do @options[:expire_after] = 1 get '/' @@ -101,28 +94,6 @@ def extract_time(sid) get '/' expect(last_response['Set-Cookie']).to be_nil end - - it 'adds the created at attribute for a new session' do - get '/' - expect(last_request.env['dynamo_db.new_session']).to eq('true') - sid = last_response['Set-Cookie'].split(/[;\=]/)[1] - time = extract_time(sid) - expect(time).to be_within(2).of(Time.now) - - get '/' - expect(last_request.env['dynamo_db.new_session']).to be_nil - end - - it 'releases pessimistic lock at finish of transaction' do - @options[:enable_locking] = true - get '/' - expect(last_request.env['dynamo_db.new_session']).to eq('true') - sid = last_response['Set-Cookie'].split(/[;\=]/)[1] - - get '/' - options = table_opts(sid).merge(attr_opts) - expect(client.get_item(options)[:item]['locked_at']).to be_nil - end end end end diff --git a/spec/aws/session_store/dynamo_db/rack_middleware_spec.rb b/spec/aws/session_store/dynamo_db/rack_middleware_spec.rb index 0b7f7d3..7d5214f 100644 --- a/spec/aws/session_store/dynamo_db/rack_middleware_spec.rb +++ b/spec/aws/session_store/dynamo_db/rack_middleware_spec.rb @@ -35,14 +35,11 @@ def ensure_data_updated(mutated_data) before do @options = { - dynamo_db_client: dynamo_db_client, - secret_key: 'watermelon_cherries' + dynamo_db_client: dynamo_db_client } end - let(:base_app) { MultiplierApplication.new } - let(:app) { RackMiddleware.new(base_app, @options) } - + let(:app) { RoutedRackApp.build(@options) } let(:sample_packed_data) do [Marshal.dump('multiplier' => 1)].pack('m*') end @@ -53,7 +50,8 @@ def ensure_data_updated(mutated_data) delete_item: 'Deleted', list_tables: { table_names: ['Sessions'] }, get_item: { item: { 'data' => sample_packed_data } }, - update_item: { attributes: { created_at: 'now' } } + put_item: { attributes: { created_at: 'now' } }, + update_item: { attributes: { updated_at: 'now' } } ) end @@ -133,12 +131,12 @@ def ensure_data_updated(mutated_data) end it "doesn't resend unmutated data" do - ensure_data_updated(true) - @options[:renew] = true get '/' ensure_data_updated(false) - get '/', {}, { 'rack.session' => { 'multiplier' => nil } } + session_cookie = last_response['Set-Cookie'] + + get '/', { 'HTTP_Cookie' => session_cookie } end end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 62e87d0..138cac4 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -24,11 +24,18 @@ $LOAD_PATH << File.join(File.dirname(File.dirname(__FILE__)), 'lib') require 'rspec' +require 'active_support' +require 'action_dispatch' require 'aws-sessionstore-dynamodb' require 'rack/test' +require 'debug' # Default Rack application class MultiplierApplication + def initialize(app, options = {}) + @app = app + end + def call(env) if env['rack.session'][:multiplier] env['rack.session'][:multiplier] *= 2 @@ -39,6 +46,30 @@ def call(env) end end +class RoutedRackApp + attr_reader :routes + + def self.build(options) + self.new(ActionDispatch::Routing::RouteSet.new) do |middleware| + middleware.use ActionDispatch::DebugExceptions + middleware.use ActionDispatch::Callbacks + middleware.use ActionDispatch::Cookies + middleware.use ActionDispatch::Flash + middleware.use Aws::SessionStore::DynamoDB::RackMiddleware, options + middleware.use MultiplierApplication + end + end + + def initialize(routes, &blk) + @routes = routes + @stack = ActionDispatch::MiddlewareStack.new(&blk).build(@routes) + end + + def call(env) + @stack.call(env) + end +end + ConstantHelpers = lambda do let(:token_error_msg) { 'The security token included in the request is invalid' } let(:resource_error) do @@ -52,9 +83,7 @@ def call(env) end let(:client_error_msg) { 'Unrecognized Client.'} let(:invalid_cookie) { { 'HTTP_COOKIE' => 'rack.session=ApplePieBlueberries' } } - let(:invalid_session_data) { { 'rack.session' => { 'multiplier' => 1 } } } let(:rack_default_error_msg) { "Warning! Aws::SessionStore::DynamoDB failed to save session. Content dropped.\n" } - let(:missing_key_error) { Aws::SessionStore::DynamoDB::MissingSecretKeyError } end RSpec.configure do |c| From 238476f3b27b8eb972d2e814bf573ffa89318a83 Mon Sep 17 00:00:00 2001 From: ryokdy Date: Fri, 10 Nov 2023 11:48:04 +0900 Subject: [PATCH 18/30] Sid may be nil when testing with RSpec --- lib/aws/session_store/dynamo_db/rack_middleware.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/aws/session_store/dynamo_db/rack_middleware.rb b/lib/aws/session_store/dynamo_db/rack_middleware.rb index ec63939..5cdf2cd 100644 --- a/lib/aws/session_store/dynamo_db/rack_middleware.rb +++ b/lib/aws/session_store/dynamo_db/rack_middleware.rb @@ -37,6 +37,7 @@ def generate_unique_sid(env, session) end def write_session(req, sid, session, options) + sid = generate_sid if sid.nil? || !sid.respond_to?(:private_id) @lock.set_session_data(req.env, sid.private_id, session, options) sid end From 02c4bb0e314c23ec8da04bca13e9feb0d53311db Mon Sep 17 00:00:00 2001 From: ryokdy Date: Thu, 16 Nov 2023 09:55:12 +0900 Subject: [PATCH 19/30] Resolve race condition issues when using one-time passwords --- lib/aws/session_store/dynamo_db/rack_middleware.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/aws/session_store/dynamo_db/rack_middleware.rb b/lib/aws/session_store/dynamo_db/rack_middleware.rb index 5cdf2cd..a2483d5 100644 --- a/lib/aws/session_store/dynamo_db/rack_middleware.rb +++ b/lib/aws/session_store/dynamo_db/rack_middleware.rb @@ -25,7 +25,6 @@ def find_session(req, sid) else unless sid and session = @lock.get_session_data(req.env, sid.private_id) session = {} - sid = generate_unique_sid(req.env, session) end [sid, session] end From 555cdb83378b4286ee522322e9ccb68ddee308c1 Mon Sep 17 00:00:00 2001 From: ryokdy Date: Mon, 20 Nov 2023 12:57:09 +0900 Subject: [PATCH 20/30] Revert 02c4bb0e --- lib/aws/session_store/dynamo_db/rack_middleware.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/aws/session_store/dynamo_db/rack_middleware.rb b/lib/aws/session_store/dynamo_db/rack_middleware.rb index a2483d5..5cdf2cd 100644 --- a/lib/aws/session_store/dynamo_db/rack_middleware.rb +++ b/lib/aws/session_store/dynamo_db/rack_middleware.rb @@ -25,6 +25,7 @@ def find_session(req, sid) else unless sid and session = @lock.get_session_data(req.env, sid.private_id) session = {} + sid = generate_unique_sid(req.env, session) end [sid, session] end From 791ead732f4ffb6f2374d377977ed3bbd03e1767 Mon Sep 17 00:00:00 2001 From: ryokdy Date: Fri, 24 Nov 2023 10:30:56 +0900 Subject: [PATCH 21/30] Remove LockWaitTimeoutError --- lib/aws-sessionstore-dynamodb.rb | 1 - lib/aws/session_store/dynamo_db/configuration.rb | 6 +----- lib/aws/session_store/dynamo_db/errors/default_handler.rb | 3 +-- lib/aws/session_store/dynamo_db/lock_wait_timeout_error.rb | 7 ------- 4 files changed, 2 insertions(+), 15 deletions(-) delete mode 100644 lib/aws/session_store/dynamo_db/lock_wait_timeout_error.rb diff --git a/lib/aws-sessionstore-dynamodb.rb b/lib/aws-sessionstore-dynamodb.rb index 3801075..731a89c 100644 --- a/lib/aws-sessionstore-dynamodb.rb +++ b/lib/aws-sessionstore-dynamodb.rb @@ -6,7 +6,6 @@ module DynamoDB; end require 'aws/session_store/dynamo_db/configuration' require 'aws/session_store/dynamo_db/invalid_id_error' -require 'aws/session_store/dynamo_db/lock_wait_timeout_error' require 'aws/session_store/dynamo_db/errors/base_handler' require 'aws/session_store/dynamo_db/errors/default_handler' require 'aws/session_store/dynamo_db/garbage_collection' diff --git a/lib/aws/session_store/dynamo_db/configuration.rb b/lib/aws/session_store/dynamo_db/configuration.rb index ed741a4..eb963a4 100644 --- a/lib/aws/session_store/dynamo_db/configuration.rb +++ b/lib/aws/session_store/dynamo_db/configuration.rb @@ -40,11 +40,7 @@ class Configuration :write_capacity => 5, :raise_errors => false, # :max_age => 7*3600*24, - # :max_stale => 3600*5, - :enable_locking => false, - :lock_expiry_time => 500, - :lock_retry_delay => 500, - :lock_max_wait_time => 1 + # :max_stale => 3600*5 } ### Feature options diff --git a/lib/aws/session_store/dynamo_db/errors/default_handler.rb b/lib/aws/session_store/dynamo_db/errors/default_handler.rb index 2e9fbfe..7895708 100644 --- a/lib/aws/session_store/dynamo_db/errors/default_handler.rb +++ b/lib/aws/session_store/dynamo_db/errors/default_handler.rb @@ -4,8 +4,7 @@ class DefaultHandler < Aws::SessionStore::DynamoDB::Errors::BaseHandler # Array of errors that will always be passed up the Rack stack. HARD_ERRORS = [ Aws::DynamoDB::Errors::ResourceNotFoundException, - Aws::DynamoDB::Errors::ConditionalCheckFailedException, - Aws::SessionStore::DynamoDB::LockWaitTimeoutError + Aws::DynamoDB::Errors::ConditionalCheckFailedException ] # Determines behavior of DefaultErrorHandler diff --git a/lib/aws/session_store/dynamo_db/lock_wait_timeout_error.rb b/lib/aws/session_store/dynamo_db/lock_wait_timeout_error.rb deleted file mode 100644 index 15ca039..0000000 --- a/lib/aws/session_store/dynamo_db/lock_wait_timeout_error.rb +++ /dev/null @@ -1,7 +0,0 @@ -module Aws::SessionStore::DynamoDB - class LockWaitTimeoutError < RuntimeError - def initialize(msg = 'Maximum time spent to acquire lock has been exceeded!') - super - end - end -end From 2ad11f45992df1a8b49a4d443d250187bfac7c17 Mon Sep 17 00:00:00 2001 From: ryokdy Date: Mon, 27 Nov 2023 11:51:22 +0900 Subject: [PATCH 22/30] Update Rack version to v2.0.8+ --- aws-sessionstore-dynamodb.gemspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aws-sessionstore-dynamodb.gemspec b/aws-sessionstore-dynamodb.gemspec index bad85a9..ecf1bb8 100644 --- a/aws-sessionstore-dynamodb.gemspec +++ b/aws-sessionstore-dynamodb.gemspec @@ -17,6 +17,6 @@ Gem::Specification.new do |spec| spec.add_dependency 'aws-sdk-dynamodb', '~> 1', '>= 1.85.0' spec.add_dependency('actionpack', '>= 6.1') - spec.add_dependency 'rack', '~> 3' + spec.add_dependency 'rack', '>= 2.0.8', '< 4' spec.add_dependency 'rack-session', '>= 2.0.0' end From 899f68eb33f82140063dd1d87ffe59fa2a2794b5 Mon Sep 17 00:00:00 2001 From: ryokdy Date: Mon, 27 Nov 2023 12:03:33 +0900 Subject: [PATCH 23/30] Remove debug gem --- Gemfile | 1 - 1 file changed, 1 deletion(-) diff --git a/Gemfile b/Gemfile index 537eb41..5df1b50 100644 --- a/Gemfile +++ b/Gemfile @@ -10,7 +10,6 @@ group :docs do end group :test do - gem 'debug' gem 'rspec' gem 'simplecov', require: false gem 'rack-test' From b6eb6840f0047106972f90b00bb8cb12b3d92db4 Mon Sep 17 00:00:00 2001 From: ryokdy Date: Mon, 27 Nov 2023 12:05:25 +0900 Subject: [PATCH 24/30] Remove debug gem --- spec/spec_helper.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 138cac4..6e3f9d0 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -28,7 +28,6 @@ require 'action_dispatch' require 'aws-sessionstore-dynamodb' require 'rack/test' -require 'debug' # Default Rack application class MultiplierApplication From 336c2d5d4f1f650b5be1258fa7a0725eff62116d Mon Sep 17 00:00:00 2001 From: ryokdy Date: Wed, 29 Nov 2023 12:06:40 +0900 Subject: [PATCH 25/30] Add support for legacy format session ids --- README.md | 2 +- lib/aws-sessionstore-dynamodb.rb | 1 + .../session_store/dynamo_db/configuration.rb | 8 +++++- .../dynamo_db/errors/default_handler.rb | 3 +- .../dynamo_db/missing_secret_key_error.rb | 7 +++++ .../dynamo_db/rack_middleware.rb | 20 ++++++++++++- .../dynamo_db/rack_middleware_spec.rb | 28 ++++++++++++++++++- 7 files changed, 64 insertions(+), 5 deletions(-) create mode 100644 lib/aws/session_store/dynamo_db/missing_secret_key_error.rb diff --git a/README.md b/README.md index f0ea3a5..8212359 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ Run the session store as a Rack middleware in the following way: use Aws::SessionStore::DynamoDB::RackMiddleware.new(options) run SomeRackApp -Note that `:secret_key` is a mandatory configuration option that must be set. +Note that `:secret_key` is a configuration option that is used for the older version. ## Detailed Usage diff --git a/lib/aws-sessionstore-dynamodb.rb b/lib/aws-sessionstore-dynamodb.rb index 731a89c..27f7f6f 100644 --- a/lib/aws-sessionstore-dynamodb.rb +++ b/lib/aws-sessionstore-dynamodb.rb @@ -6,6 +6,7 @@ module DynamoDB; end require 'aws/session_store/dynamo_db/configuration' require 'aws/session_store/dynamo_db/invalid_id_error' +require 'aws/session_store/dynamo_db/missing_secret_key_error' require 'aws/session_store/dynamo_db/errors/base_handler' require 'aws/session_store/dynamo_db/errors/default_handler' require 'aws/session_store/dynamo_db/garbage_collection' diff --git a/lib/aws/session_store/dynamo_db/configuration.rb b/lib/aws/session_store/dynamo_db/configuration.rb index 5532ce4..28cce60 100644 --- a/lib/aws/session_store/dynamo_db/configuration.rb +++ b/lib/aws/session_store/dynamo_db/configuration.rb @@ -40,7 +40,8 @@ class Configuration :write_capacity => 5, :raise_errors => false, # :max_age => 7*3600*24, - # :max_stale => 3600*5 + # :max_stale => 3600*5, + :secret_key => nil } ### Feature options @@ -81,6 +82,9 @@ class Configuration # before the current time that the session was last accessed. attr_reader :max_stale + # @return [String] The secret key for HMAC encryption of legacy sid. + attr_reader :secret_key + # @return [String,Pathname] attr_reader :config_file @@ -126,6 +130,8 @@ class Configuration # from the current time that a session was created. # @option options [Integer] :max_stale (nil) Maximum number of seconds # before current time that session was last accessed. + # @option options [String] :secret_key (SecureRandom.hex(64)) + # Secret key for HMAC encription. def initialize(options = {}) @options = default_options.merge( env_options.merge( diff --git a/lib/aws/session_store/dynamo_db/errors/default_handler.rb b/lib/aws/session_store/dynamo_db/errors/default_handler.rb index 7895708..27d8c09 100644 --- a/lib/aws/session_store/dynamo_db/errors/default_handler.rb +++ b/lib/aws/session_store/dynamo_db/errors/default_handler.rb @@ -4,7 +4,8 @@ class DefaultHandler < Aws::SessionStore::DynamoDB::Errors::BaseHandler # Array of errors that will always be passed up the Rack stack. HARD_ERRORS = [ Aws::DynamoDB::Errors::ResourceNotFoundException, - Aws::DynamoDB::Errors::ConditionalCheckFailedException + Aws::DynamoDB::Errors::ConditionalCheckFailedException, + Aws::SessionStore::DynamoDB::MissingSecretKeyError ] # Determines behavior of DefaultErrorHandler diff --git a/lib/aws/session_store/dynamo_db/missing_secret_key_error.rb b/lib/aws/session_store/dynamo_db/missing_secret_key_error.rb new file mode 100644 index 0000000..ed13be0 --- /dev/null +++ b/lib/aws/session_store/dynamo_db/missing_secret_key_error.rb @@ -0,0 +1,7 @@ +module Aws::SessionStore::DynamoDB + class MissingSecretKeyError < RuntimeError + def initialize(msg = "No secret key provided!") + super + end + end +end diff --git a/lib/aws/session_store/dynamo_db/rack_middleware.rb b/lib/aws/session_store/dynamo_db/rack_middleware.rb index 5cdf2cd..2e26cbd 100644 --- a/lib/aws/session_store/dynamo_db/rack_middleware.rb +++ b/lib/aws/session_store/dynamo_db/rack_middleware.rb @@ -23,7 +23,8 @@ def find_session(req, sid) if req.session.options[:skip] [generate_sid, {}] else - unless sid and session = @lock.get_session_data(req.env, sid.private_id) + session = find_session_data(req, sid) + unless session session = {} sid = generate_unique_sid(req.env, session) end @@ -31,6 +32,23 @@ def find_session(req, sid) end end + # Generate HMAC hash based on MD5 + def generate_hmac(sid, secret) + OpenSSL::HMAC.hexdigest(OpenSSL::Digest::MD5.new, secret, sid).strip() + end + + # Get session data from DynamoDB. + def find_session_data(req, sid) + return nil unless sid + digest, ver_sid = sid.public_id.split("--") + if ver_sid && @config.secret_key && digest == generate_hmac(ver_sid, @config.secret_key) + # Legacy session id format + @lock.get_session_data(req.env, sid.public_id) + else + @lock.get_session_data(req.env, sid.private_id) + end + end + def generate_unique_sid(env, session) env['dynamo_db.new_session'] = 'true' generate_sid diff --git a/spec/aws/session_store/dynamo_db/rack_middleware_spec.rb b/spec/aws/session_store/dynamo_db/rack_middleware_spec.rb index 110befc..1823d9e 100644 --- a/spec/aws/session_store/dynamo_db/rack_middleware_spec.rb +++ b/spec/aws/session_store/dynamo_db/rack_middleware_spec.rb @@ -35,10 +35,12 @@ def ensure_data_updated(mutated_data) before do @options = { - dynamo_db_client: dynamo_db_client + dynamo_db_client: dynamo_db_client, + secret_key: secret_key } end + let(:secret_key) { 'watermelon_cherries' } let(:app) { RoutedRackApp.build(@options) } let(:sample_packed_data) do [Marshal.dump('multiplier' => 1)].pack('m*') @@ -140,6 +142,30 @@ def ensure_data_updated(mutated_data) get '/', { 'HTTP_Cookie' => session_cookie } end end + + describe 'Legacy session id format' do + let(:legacy_session_id) do + sid = SecureRandom.hex(16) + sid.encode!(Encoding::UTF_8) + "#{OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('MD5'), secret_key, sid).strip}--" + sid + end + + it 'loads/manipulates a session based on legacy session id' do + expect(dynamo_db_client).to receive(:get_item).with( + { + :attributes_to_get => ["data"], + :consistent_read => true, + :key => { + "session_id" => legacy_session_id + }, + :table_name=>"sessions" + } + ) + set_cookie("_session_id=#{legacy_session_id}; path=/; httponly") + get '/' + expect(last_request.session.to_hash).to eq('multiplier' => 2) + end + end end end end From 51aa4003044168fdc62cb339f1de1e785f2abec7 Mon Sep 17 00:00:00 2001 From: ryokdy Date: Wed, 29 Nov 2023 12:11:32 +0900 Subject: [PATCH 26/30] Organize alphabetically --- aws-sessionstore-dynamodb.gemspec | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/aws-sessionstore-dynamodb.gemspec b/aws-sessionstore-dynamodb.gemspec index ecf1bb8..c8f2315 100644 --- a/aws-sessionstore-dynamodb.gemspec +++ b/aws-sessionstore-dynamodb.gemspec @@ -15,8 +15,9 @@ Gem::Specification.new do |spec| spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) spec.require_paths = ["lib"] - spec.add_dependency 'aws-sdk-dynamodb', '~> 1', '>= 1.85.0' + # Require 1.85.0 for user_agent_frameworks config spec.add_dependency('actionpack', '>= 6.1') + spec.add_dependency 'aws-sdk-dynamodb', '~> 1', '>= 1.85.0' spec.add_dependency 'rack', '>= 2.0.8', '< 4' spec.add_dependency 'rack-session', '>= 2.0.0' end From b7121961af1fe7cc6bff01a89491aed5eb9a5ef3 Mon Sep 17 00:00:00 2001 From: ryokdy Date: Wed, 29 Nov 2023 13:17:28 +0900 Subject: [PATCH 27/30] Restore threaded_session_spec --- .../locking/threaded_sessions_spec.rb | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 spec/aws/session_store/dynamo_db/locking/threaded_sessions_spec.rb diff --git a/spec/aws/session_store/dynamo_db/locking/threaded_sessions_spec.rb b/spec/aws/session_store/dynamo_db/locking/threaded_sessions_spec.rb new file mode 100644 index 0000000..5943c01 --- /dev/null +++ b/spec/aws/session_store/dynamo_db/locking/threaded_sessions_spec.rb @@ -0,0 +1,68 @@ +# frozen_string_literal: true + +# Copyright 2013 Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +# ANY KIND, either express or implied. See the License for the specific +# language governing permissions and limitations under the License. + +require 'spec_helper' + +describe Aws::SessionStore::DynamoDB::RackMiddleware do + include Rack::Test::Methods + + def thread(mul_val, time, check) + Thread.new do + sleep(time) + get '/' + expect(last_request.session[:multiplier]).to eq(mul_val) if check + end + end + + def thread_exception(error) + Thread.new { expect { get '/' }.to raise_error(error) } + end + + def update_item_mock(options, update_method) + if options[:return_values] == 'UPDATED_NEW' && options.key?(:expected) + sleep(0.50) + update_method.call(options) + else + update_method.call(options) + end + end + + let(:app) { RoutedRackApp.build(@options) } + + context 'Mock Multiple Threaded Sessions', integration: true do + before do + @options = Aws::SessionStore::DynamoDB::Configuration.new.to_hash + @options[:enable_locking] = true + @options[:secret_key] = 'watermelon_smiles' + + update_method = @options[:dynamo_db_client].method(:update_item) + expect(@options[:dynamo_db_client]).to receive(:update_item).at_least(:once) do |options| + update_item_mock(options, update_method) + end + end + + it 'should wait for lock' do + @options[:lock_expiry_time] = 2000 + + get '/' + expect(last_request.session[:multiplier]).to eq(1) + + t1 = thread(2, 0, false) + t2 = thread(4, 0.25, true) + t1.join + t2.join + end + end +end From 31268443fc27d4487a5b74beac522902feaabdd4 Mon Sep 17 00:00:00 2001 From: ryokdy Date: Wed, 29 Nov 2023 13:36:59 +0900 Subject: [PATCH 28/30] Change required rack-session to v1.0.1 --- aws-sessionstore-dynamodb.gemspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aws-sessionstore-dynamodb.gemspec b/aws-sessionstore-dynamodb.gemspec index c8f2315..b00588e 100644 --- a/aws-sessionstore-dynamodb.gemspec +++ b/aws-sessionstore-dynamodb.gemspec @@ -19,5 +19,5 @@ Gem::Specification.new do |spec| spec.add_dependency('actionpack', '>= 6.1') spec.add_dependency 'aws-sdk-dynamodb', '~> 1', '>= 1.85.0' spec.add_dependency 'rack', '>= 2.0.8', '< 4' - spec.add_dependency 'rack-session', '>= 2.0.0' + spec.add_dependency 'rack-session', '>= 1.0.1' end From 8cbf185ea94f42317986577d91af2030359273db Mon Sep 17 00:00:00 2001 From: ryokdy Date: Wed, 29 Nov 2023 14:07:12 +0900 Subject: [PATCH 29/30] Fix bug preventing saving of sessions with legacy format sid --- .../dynamo_db/rack_middleware.rb | 28 +++++++++---------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/lib/aws/session_store/dynamo_db/rack_middleware.rb b/lib/aws/session_store/dynamo_db/rack_middleware.rb index 2e26cbd..e3aa44a 100644 --- a/lib/aws/session_store/dynamo_db/rack_middleware.rb +++ b/lib/aws/session_store/dynamo_db/rack_middleware.rb @@ -23,8 +23,7 @@ def find_session(req, sid) if req.session.options[:skip] [generate_sid, {}] else - session = find_session_data(req, sid) - unless session + unless sid and session = @lock.get_session_data(req.env, get_session_id_with_fallback(sid)) session = {} sid = generate_unique_sid(req.env, session) end @@ -37,31 +36,30 @@ def generate_hmac(sid, secret) OpenSSL::HMAC.hexdigest(OpenSSL::Digest::MD5.new, secret, sid).strip() end - # Get session data from DynamoDB. - def find_session_data(req, sid) + def generate_unique_sid(env, session) + env['dynamo_db.new_session'] = 'true' + generate_sid + end + + def get_session_id_with_fallback(sid) return nil unless sid - digest, ver_sid = sid.public_id.split("--") + digest, ver_sid = sid.public_id.split('--') if ver_sid && @config.secret_key && digest == generate_hmac(ver_sid, @config.secret_key) # Legacy session id format - @lock.get_session_data(req.env, sid.public_id) + sid.public_id else - @lock.get_session_data(req.env, sid.private_id) + sid.private_id end end - def generate_unique_sid(env, session) - env['dynamo_db.new_session'] = 'true' - generate_sid - end - def write_session(req, sid, session, options) - sid = generate_sid if sid.nil? || !sid.respond_to?(:private_id) - @lock.set_session_data(req.env, sid.private_id, session, options) + sid = generate_sid if sid.nil? + @lock.set_session_data(req.env, get_session_id_with_fallback(sid), session, options) sid end def delete_session(req, sid, options) - @lock.delete_session(req.env, sid.private_id) + @lock.delete_session(req.env, get_session_id_with_fallback(sid)) generate_sid unless options[:drop] end end From af58c09cf8dab9c49b9b2dcf38dca4f9b63514b5 Mon Sep 17 00:00:00 2001 From: ryokdy Date: Wed, 29 Nov 2023 17:31:35 +0900 Subject: [PATCH 30/30] Avoid to create a new sid when writing sessin --- lib/aws/session_store/dynamo_db/rack_middleware.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/aws/session_store/dynamo_db/rack_middleware.rb b/lib/aws/session_store/dynamo_db/rack_middleware.rb index e3aa44a..aab5a3b 100644 --- a/lib/aws/session_store/dynamo_db/rack_middleware.rb +++ b/lib/aws/session_store/dynamo_db/rack_middleware.rb @@ -43,6 +43,7 @@ def generate_unique_sid(env, session) def get_session_id_with_fallback(sid) return nil unless sid + digest, ver_sid = sid.public_id.split('--') if ver_sid && @config.secret_key && digest == generate_hmac(ver_sid, @config.secret_key) # Legacy session id format @@ -53,7 +54,6 @@ def get_session_id_with_fallback(sid) end def write_session(req, sid, session, options) - sid = generate_sid if sid.nil? @lock.set_session_data(req.env, get_session_id_with_fallback(sid), session, options) sid end