From 065859b90cc5ef403b8f47bd5394b343e556cc4d Mon Sep 17 00:00:00 2001 From: Azul Date: Wed, 23 Mar 2016 21:48:52 +0100 Subject: upgrade: remove references to RestClient CouchRest > 1.2 does not use RestClient anymore. So we should not try to catch its errors. --- .../lib/couchrest/model/database_method.rb | 131 ++++++++++ .../lib/couchrest/model/rotation.rb | 263 +++++++++++++++++++++ .../lib/couchrest/session/document.rb | 119 ++++++++++ .../lib/couchrest/session/store.rb | 94 ++++++++ .../test/database_method_test.rb | 116 +++++++++ .../test/session_store_test.rb | 168 +++++++++++++ 6 files changed, 891 insertions(+) create mode 100644 vendor/gems/couchrest_session_store/lib/couchrest/model/database_method.rb create mode 100644 vendor/gems/couchrest_session_store/lib/couchrest/model/rotation.rb create mode 100644 vendor/gems/couchrest_session_store/lib/couchrest/session/document.rb create mode 100644 vendor/gems/couchrest_session_store/lib/couchrest/session/store.rb create mode 100644 vendor/gems/couchrest_session_store/test/database_method_test.rb create mode 100644 vendor/gems/couchrest_session_store/test/session_store_test.rb (limited to 'vendor/gems') diff --git a/vendor/gems/couchrest_session_store/lib/couchrest/model/database_method.rb b/vendor/gems/couchrest_session_store/lib/couchrest/model/database_method.rb new file mode 100644 index 0000000..6ecc8f3 --- /dev/null +++ b/vendor/gems/couchrest_session_store/lib/couchrest/model/database_method.rb @@ -0,0 +1,131 @@ +# +# Allow setting the database to happen dynamically. +# +# Unlike normal CouchRest::Model, the database is not automatically created +# unless you call database!() +# +# The method specified by `database_method` must exist as a class method but +# may optionally also exist as an instance method. +# + +module CouchRest + module Model + module DatabaseMethod + extend ActiveSupport::Concern + + def database + if self.class.database_method + self.class.server.database(call_database_method) + else + self.class.database + end + end + + def database! + if self.class.database_method + self.class.server.database!(call_database_method) + else + self.class.database! + end + end + + def database_exists?(db_name) + self.class.database_exists?(db_name) + end + + # + # The normal CouchRest::Model::Base comparison checks if the model's + # database objects are the same. That is not good for use here, since + # the objects will always be different. Instead, we compare the string + # that each database evaluates to. + # + def ==(other) + return false unless other.is_a?(Base) + if id.nil? && other.id.nil? + to_hash == other.to_hash + else + id == other.id && database.to_s == other.database.to_s + end + end + alias :eql? :== + + protected + + def call_database_method + if self.respond_to?(self.class.database_method) + name = self.send(self.class.database_method) + self.class.db_name_with_prefix(name) + else + self.class.send(:call_database_method) + end + end + + module ClassMethods + + def database_method(method = nil) + if method + @database_method = method + end + @database_method + end + alias :use_database_method :database_method + + def database + if database_method + if !self.respond_to?(database_method) + raise ArgumentError.new("Incorrect argument to database_method(): no such method '#{method}' found in class #{self}.") + end + self.server.database(call_database_method) + else + @database ||= prepare_database(super) + end + end + + def database! + if database_method + self.server.database!(call_database_method) + else + @database ||= prepare_database(super) + end + end + + # + # same as database(), but allows for an argument that gets passed through to + # database method. + # + def choose_database(*args) + self.server.database(call_database_method(*args)) + end + + def db_name_with_prefix(name) + conf = self.send(:connection_configuration) + [conf[:prefix], name, conf[:suffix]].reject{|i|i.to_s.empty?}.join(conf[:join]) + end + + def database_exists?(name) + name = db_name_with_prefix(name) + begin + CouchRest.head "#{self.server.uri}/#{name}" + return true + rescue CouchRest::NotFound + return false + end + end + + protected + + def call_database_method(*args) + name = nil + method = self.method(database_method) + if method.arity == 0 + name = method.call + else + name = method.call(*args) + end + db_name_with_prefix(name) + end + + end + end + end +end diff --git a/vendor/gems/couchrest_session_store/lib/couchrest/model/rotation.rb b/vendor/gems/couchrest_session_store/lib/couchrest/model/rotation.rb new file mode 100644 index 0000000..9e1a5c3 --- /dev/null +++ b/vendor/gems/couchrest_session_store/lib/couchrest/model/rotation.rb @@ -0,0 +1,263 @@ +module CouchRest + module Model + module Rotation + extend ActiveSupport::Concern + include CouchRest::Model::DatabaseMethod + + included do + use_database_method :rotated_database_name + end + + def create(*args) + super(*args) + rescue CouchRest::NotFound => exc + raise storage_missing(exc) + end + + def update(*args) + super(*args) + rescue CouchRest::NotFound => exc + raise storage_missing(exc) + end + + def destroy(*args) + super(*args) + rescue CouchRest::NotFound => exc + raise storage_missing(exc) + end + + private + + # returns a special 'storage missing' exception when the db has + # not been created. very useful, since this happens a lot and a + # generic 404 is not that helpful. + def storage_missing(exc) + if exc.http_body =~ /no_db_file/ + CouchRest::StorageMissing.new(exc.response, database) + else + exc + end + end + + public + + module ClassMethods + # + # Set up database rotation. + # + # base_name -- the name of the db before the rotation number is + # appended. + # + # options -- one of: + # + # * :every -- frequency of rotation + # * :expiration_field - what field to use to determine if a + # document is expired. + # * :timestamp_field - alternately, what field to use for the + # document timestamp. + # * :timeout -- used to expire documents with only a timestamp + # field (in minutes) + # + def rotate_database(base_name, options={}) + @rotation_base_name = base_name + @rotation_every = (options.delete(:every) || 30.days).to_i + @expiration_field = options.delete(:expiration_field) + @timestamp_field = options.delete(:timestamp_field) + @timeout = options.delete(:timeout) + if options.any? + raise ArgumentError.new('Could not understand options %s' % options.keys) + end + end + + # + # Check to see if dbs should be rotated. The :window + # argument specifies how far in advance we should + # create the new database (default 1.day). + # + # This method relies on the assumption that it is called + # at least once within each @rotation_every period. + # + def rotate_database_now(options={}) + window = options[:window] || 1.day + + now = Time.now.utc + current_name = rotated_database_name(now) + current_count = now.to_i/@rotation_every + + next_time = window.from_now.utc + next_name = rotated_database_name(next_time) + next_count = current_count+1 + + prev_name = current_name.sub(/(\d+)$/) {|i| i.to_i-1} + replication_started = false + old_name = prev_name.sub(/(\d+)$/) {|i| i.to_i-1} # even older than prev_name + trailing_edge_time = window.ago.utc + + if !database_exists?(current_name) + # we should have created the current db earlier, but if somehow + # it is missing we must make sure it exists. + create_new_rotated_database(:from => prev_name, :to => current_name) + replication_started = true + end + + if next_time.to_i/@rotation_every >= next_count && !database_exists?(next_name) + # time to create the next db in advance of actually needing it. + create_new_rotated_database(:from => current_name, :to => next_name) + end + + if trailing_edge_time.to_i/@rotation_every == current_count + # delete old dbs, but only after window time has past since the last rotation + if !replication_started && database_exists?(prev_name) + # delete previous, but only if we didn't just start replicating from it + self.server.database(db_name_with_prefix(prev_name)).delete! + end + if database_exists?(old_name) + # there are some edge cases, when rotate_database_now is run + # infrequently, that an older db might be left around. + self.server.database(db_name_with_prefix(old_name)).delete! + end + end + end + + def rotated_database_name(time=nil) + unless @rotation_base_name && @rotation_every + raise ArgumentError.new('missing @rotation_base_name or @rotation_every') + end + time ||= Time.now.utc + units = time.to_i / @rotation_every.to_i + "#{@rotation_base_name}_#{units}" + end + + # + # create a new empty database. + # + def create_database!(name=nil) + db = if name + self.server.database!(db_name_with_prefix(name)) + else + self.database! + end + create_rotation_filter(db) + if self.respond_to?(:design_doc) + design_doc.sync!(db) + # or maybe this?: + #self.design_docs.each do |design| + # design.migrate(to_db) + #end + end + return db + end + + protected + + # + # Creates database named by options[:to]. Optionally, set up + # continuous replication from the options[:from] db, if it exists. The + # assumption is that the from db will be destroyed later, cleaning up + # the replication once it is no longer needed. + # + # This method will also copy design documents if present in the from + # db, in the CouchRest::Model, or in a database named after + # @rotation_base_name. + # + def create_new_rotated_database(options={}) + from = options[:from] + to = options[:to] + to_db = self.create_database!(to) + if database_exists?(@rotation_base_name) + base_db = self.server.database(db_name_with_prefix(@rotation_base_name)) + copy_design_docs(base_db, to_db) + end + if from && from != to && database_exists?(from) + from_db = self.server.database(db_name_with_prefix(from)) + replicate_old_to_new(from_db, to_db) + end + end + + def copy_design_docs(from, to) + params = {:startkey => '_design/', :endkey => '_design0', :include_docs => true} + from.documents(params) do |doc_hash| + design = doc_hash['doc'] + begin + to.get(design['_id']) + rescue CouchRest::NotFound + design.delete('_rev') + to.save_doc(design) + end + end + end + + def create_rotation_filter(db) + name = 'rotation_filter' + filter_string = if @expiration_field + NOT_EXPIRED_FILTER % {:expires => @expiration_field} + elsif @timestamp_field && @timeout + NOT_TIMED_OUT_FILTER % {:timestamp => @timestamp_field, :timeout => (60 * @timeout)} + else + NOT_DELETED_FILTER + end + filters = {"not_expired" => filter_string} + db.save_doc("_id" => "_design/#{name}", "filters" => filters) + rescue CouchRest::Conflict + end + + # + # Replicates documents from_db to to_db, skipping documents that have + # expired or been deleted. + # + # NOTE: It would be better if we could do this: + # + # from_db.replicate_to(to_db, true, false, + # :filter => 'rotation_filter/not_expired') + # + # But replicate_to() does not support a filter argument, so we call + # the private method replication() directly. + # + def replicate_old_to_new(from_db, to_db) + create_rotation_filter(from_db) + from_db.send(:replicate, to_db, true, :source => from_db.name, :filter => 'rotation_filter/not_expired') + end + + # + # Three different filters, depending on how the model is set up. + # + # NOT_EXPIRED_FILTER is used when there is a single field that + # contains an absolute time for when the document has expired. The + # + # NOT_TIMED_OUT_FILTER is used when there is a field that records the + # timestamp of the last time the document was used. The expiration in + # this case is calculated from the timestamp plus @timeout. + # + # NOT_DELETED_FILTER is used when the other two cannot be. + # + NOT_EXPIRED_FILTER = "" + +%[function(doc, req) { + if (doc._deleted) { + return false; + } else if (typeof(doc.%{expires}) != "undefined") { + return Date.now() < (new Date(doc.%{expires})).getTime(); + } else { + return true; + } +}] + + NOT_TIMED_OUT_FILTER = "" + +%[function(doc, req) { + if (doc._deleted) { + return false; + } else if (typeof(doc.%{timestamp}) != "undefined") { + return Date.now() < (new Date(doc.%{timestamp})).getTime() + %{timeout}; + } else { + return true; + } +}] + + NOT_DELETED_FILTER = "" + +%[function(doc, req) { + return !doc._deleted; +}] + + end + end + end +end diff --git a/vendor/gems/couchrest_session_store/lib/couchrest/session/document.rb b/vendor/gems/couchrest_session_store/lib/couchrest/session/document.rb new file mode 100644 index 0000000..b1e73cc --- /dev/null +++ b/vendor/gems/couchrest_session_store/lib/couchrest/session/document.rb @@ -0,0 +1,119 @@ +require 'couchrest/session/utility' +require 'time' + +class CouchRest::Session::Document < CouchRest::Document + include CouchRest::Model::Configuration + include CouchRest::Model::Connection + include CouchRest::Session::Utility + include CouchRest::Model::Rotation + + rotate_database 'sessions', + :every => 1.month, :expiration_field => :expires + + def self.fetch(sid) + self.allocate.tap do |session_doc| + session_doc.fetch(sid) + end + end + + def self.build(sid, session, options = {}) + self.new(CouchRest::Document.new({"_id" => sid})).tap do |session_doc| + session_doc.update session, options + end + end + + def self.build_or_update(sid, session, options = {}) + options[:marshal_data] = true if options[:marshal_data].nil? + doc = self.fetch(sid) + doc.update(session, options) + return doc + rescue CouchRest::NotFound + self.build(sid, session, options) + end + + def self.find_by_expires(options = {}) + options[:reduce] ||= false + design = database.get '_design/Session' + response = design.view :by_expires, options + response['rows'] + end + + def self.create_database!(name=nil) + db = super(name) + begin + db.get('_design/Session') + rescue CouchRest::NotFound + design = File.read(File.expand_path('../../../../design/Session.json', __FILE__)) + design = JSON.parse(design) + db.save_doc(design.merge({"_id" => "_design/Session"})) + end + db + end + + def initialize(doc) + @doc = doc + end + + def fetch(sid = nil) + @doc = database.get(sid || doc['_id']) + end + + def to_session + if doc["marshalled"] + session = unmarshal(doc["data"]) + else + session = doc["data"] + end + return session + end + + def delete + database.delete_doc(doc) + end + + def update(session, options) + # clean up old data but leave id and revision intact + doc.reject! do |k,v| + k[0] != '_' + end + doc.merge! data_for_doc(session, options) + end + + def save + database.save_doc(doc) + rescue CouchRest::Conflict + fetch + retry + rescue CouchRest::NotFound => exc + if exc.http_body =~ /no_db_file/ + exc = CouchRest::StorageMissing.new(exc.response, database) + end + raise exc + end + + def expired? + expires && expires < Time.now + end + + protected + + def data_for_doc(session, options) + { "data" => options[:marshal_data] ? marshal(session) : session, + "marshalled" => options[:marshal_data], + "expires" => expiry_from_options(options) } + end + + def expiry_from_options(options) + expire_after = options[:expire_after] + expire_after && (Time.now + expire_after).utc + end + + def expires + doc["expires"] && Time.iso8601(doc["expires"]) + end + + def doc + @doc + end + +end diff --git a/vendor/gems/couchrest_session_store/lib/couchrest/session/store.rb b/vendor/gems/couchrest_session_store/lib/couchrest/session/store.rb new file mode 100644 index 0000000..f209f54 --- /dev/null +++ b/vendor/gems/couchrest_session_store/lib/couchrest/session/store.rb @@ -0,0 +1,94 @@ +class CouchRest::Session::Store < ActionDispatch::Session::AbstractStore + + # delegate configure to document + def self.configure(*args, &block) + CouchRest::Session::Document.configure *args, &block + end + + def self.set_options(options) + @options = options + if @options[:database] + CouchRest::Session::Document.use_database @options[:database] + end + end + + def initialize(app, options = {}) + super + self.class.set_options(options) + end + + def cleanup(rows) + rows.each do |row| + doc = CouchRest::Session::Document.fetch(row['id']) + doc.delete + end + end + + def expired + CouchRest::Session::Document.find_by_expires startkey: 1, + endkey: Time.now.utc.iso8601 + end + + def never_expiring + CouchRest::Session::Document.find_by_expires endkey: 1 + end + + private + + def get_session(env, sid) + if session = fetch_session(sid) + [sid, session] + else + [generate_sid, {}] + end + rescue CouchRest::NotFound + # session data does not exist anymore + return [sid, {}] + rescue CouchRest::Unauthorized, + Errno::EHOSTUNREACH, + Errno::ECONNREFUSED => e + # can't connect to couch. We add some status to the session + # so the app can react. (Display error for example) + return [sid, {"_status" => {"couch" => "unreachable"}}] + end + + def set_session(env, sid, session, options) + raise CouchRest::NotFound if /^_design\/(.*)/ =~ sid + doc = build_or_update_doc(sid, session, options) + doc.save + return sid + # if we can't store the session we just return false. + rescue CouchRest::Unauthorized, + Errno::EHOSTUNREACH, + Errno::ECONNREFUSED => e + return false + end + + def destroy_session(env, sid, options) + doc = secure_get(sid) + doc.delete + generate_sid unless options[:drop] + rescue CouchRest::NotFound + # already destroyed - we're done. + generate_sid unless options[:drop] + end + + def fetch_session(sid) + return nil unless sid + doc = secure_get(sid) + doc.to_session unless doc.expired? + end + + def build_or_update_doc(sid, session, options) + CouchRest::Session::Document.build_or_update(sid, session, options) + end + + # prevent access to design docs + # this should be prevented on a couch permission level as well. + # but better be save than sorry. + def secure_get(sid) + raise CouchRest::NotFound if /^_design\/(.*)/ =~ sid + CouchRest::Session::Document.fetch(sid) + end + +end diff --git a/vendor/gems/couchrest_session_store/test/database_method_test.rb b/vendor/gems/couchrest_session_store/test/database_method_test.rb new file mode 100644 index 0000000..18985c3 --- /dev/null +++ b/vendor/gems/couchrest_session_store/test/database_method_test.rb @@ -0,0 +1,116 @@ +require_relative 'test_helper' + +class DatabaseMethodTest < MiniTest::Test + + class TestModel < CouchRest::Model::Base + include CouchRest::Model::DatabaseMethod + + use_database_method :db_name + property :dbname, String + property :confirm, String + + def db_name + "test_db_#{self[:dbname]}" + end + end + + def test_instance_method + doc1 = TestModel.new({:dbname => 'one'}) + doc1.database.create! + assert doc1.database.root.ends_with?('test_db_one') + assert doc1.save + doc1.update_attributes(:confirm => 'yep') + + doc2 = TestModel.new({:dbname => 'two'}) + doc2.database.create! + assert doc2.database.root.ends_with?('test_db_two') + assert doc2.save + doc2.confirm = 'sure' + doc2.save! + + doc1_copy = CouchRest.get([doc1.database.root, doc1.id].join('/')) + assert_equal "yep", doc1_copy["confirm"] + + doc2_copy = CouchRest.get([doc2.database.root, doc2.id].join('/')) + assert_equal "sure", doc2_copy["confirm"] + + doc1.database.delete! + doc2.database.delete! + end + + def test_switch_db + doc_red = TestModel.new({:dbname => 'red', :confirm => 'rose'}) + doc_red.database.create! + root = doc_red.database.root + + doc_blue = doc_red.clone + doc_blue.dbname = 'blue' + doc_blue.database! + doc_blue.save! + + doc_blue_copy = CouchRest.get([root.sub('red','blue'), doc_blue.id].join('/')) + assert_equal "rose", doc_blue_copy["confirm"] + + doc_red.database.delete! + doc_blue.database.delete! + end + + # + # A test scenario for database_method in which some user accounts + # are stored in a seperate temporary database (so that the test + # accounts don't bloat the normal database). + # + + class User < CouchRest::Model::Base + include CouchRest::Model::DatabaseMethod + + use_database_method :db_name + property :login, String + before_save :create_db + + class << self + def get(id, db = database) + result = super(id, db) + if result.nil? + return super(id, choose_database('test-user')) + else + return result + end + end + alias :find :get + end + + protected + + def self.db_name(login = nil) + if !login.nil? && login =~ /test-user/ + 'tmp_users' + else + 'users' + end + end + + def db_name + self.class.db_name(self.login) + end + + def create_db + unless database_exists?(db_name) + self.database! + end + end + + end + + def test_tmp_user_db + user1 = User.new({:login => 'test-user-1'}) + assert user1.save + assert User.find(user1.id), 'should find user in tmp_users' + assert_equal user1.login, User.find(user1.id).login + assert_equal 'test-user-1', User.server.database('couchrest_tmp_users').get(user1.id)['login'] + assert_raises CouchRest::NotFound do + User.server.database('couchrest_users').get(user1.id) + end + end + +end diff --git a/vendor/gems/couchrest_session_store/test/session_store_test.rb b/vendor/gems/couchrest_session_store/test/session_store_test.rb new file mode 100644 index 0000000..4fbf30b --- /dev/null +++ b/vendor/gems/couchrest_session_store/test/session_store_test.rb @@ -0,0 +1,168 @@ +require File.expand_path(File.dirname(__FILE__) + '/test_helper') + +class SessionStoreTest < MiniTest::Test + + def test_session_initialization + sid, session = store.send :get_session, env, nil + assert sid + assert_equal Hash.new, session + end + + def test_normal_session_flow + sid, session = never_expiring_session + assert_equal [sid, session], store.send(:get_session, env, sid) + store.send :destroy_session, env, sid, {} + end + + def test_updating_session + sid, session = never_expiring_session + session[:bla] = "blub" + store.send :set_session, env, sid, session, {} + assert_equal [sid, session], store.send(:get_session, env, sid) + store.send :destroy_session, env, sid, {} + end + + def test_prevent_access_to_design_docs + sid = '_design/bla' + session = {views: 'my hacked view'} + assert_raises CouchRest::NotFound do + store_session(sid, session) + end + end + + def test_unmarshalled_session_flow + sid, session = init_session + store_session sid, session, :marshal_data => false + new_sid, new_session = store.send(:get_session, env, sid) + assert_equal sid, new_sid + assert_equal session[:key], new_session["key"] + store.send :destroy_session, env, sid, {} + end + + def test_unmarshalled_data + sid, session = init_session + store_session sid, session, :marshal_data => false + couch = CouchTester.new + data = couch.get(sid)["data"] + assert_equal session[:key], data["key"] + end + + def test_logout_in_between + sid, session = never_expiring_session + store.send :destroy_session, env, sid, {} + other_sid, other_session = store.send(:get_session, env, sid) + assert_equal Hash.new, other_session + end + + def test_can_logout_twice + sid, session = never_expiring_session + store.send :destroy_session, env, sid, {} + store.send :destroy_session, env, sid, {} + other_sid, other_session = store.send(:get_session, env, sid) + assert_equal Hash.new, other_session + end + + def test_stored_and_not_expired_yet + sid, session = expiring_session + doc = CouchRest::Session::Document.fetch(sid) + expires = doc.send :expires + assert expires + assert !doc.expired? + assert (expires - Time.now) > 0, "Exiry should be in the future" + assert (expires - Time.now) <= 300, "Should expire after 300 seconds - not more" + assert_equal [sid, session], store.send(:get_session, env, sid) + end + + def test_stored_but_expired + sid, session = expired_session + other_sid, other_session = store.send(:get_session, env, sid) + assert_equal Hash.new, other_session, "session should have expired" + assert other_sid != sid + end + + def test_find_expired_sessions + expired, expiring, never_expiring = seed_sessions + expired_session_ids = store.expired.map {|row| row['id']} + assert expired_session_ids.include?(expired) + assert !expired_session_ids.include?(expiring) + assert !expired_session_ids.include?(never_expiring) + end + + def test_find_never_expiring_sessions + expired, expiring, never_expiring = seed_sessions + never_expiring_session_ids = store.never_expiring.map {|row| row['id']} + assert never_expiring_session_ids.include?(never_expiring) + assert !never_expiring_session_ids.include?(expiring) + assert !never_expiring_session_ids.include?(expired) + end + + def test_cleanup_expired_sessions + sid, session = expired_session + store.cleanup(store.expired) + assert_raises CouchRest::NotFound do + CouchTester.new.get(sid) + end + end + + def test_keep_fresh_during_cleanup + sid, session = expiring_session + store.cleanup(store.expired) + assert_equal [sid, session], store.send(:get_session, env, sid) + end + + def test_store_without_expiry + sid, session = never_expiring_session + couch = CouchTester.new + assert_nil couch.get(sid)["expires"] + assert_equal [sid, session], store.send(:get_session, env, sid) + end + + def app + nil + end + + def store(options = {}) + @store ||= CouchRest::Session::Store.new(app, options) + end + + def env(settings = {}) + env ||= settings + end + + # returns the session ids of an expired, and expiring and a never + # expiring session + def seed_sessions + [expired_session, expiring_session, never_expiring_session].map(&:first) + end + + def never_expiring_session + store_session *init_session + end + + def expiring_session + sid, session = init_session + store_session(sid, session, expire_after: 300) + end + + def expired_session + expire_session *expiring_session + end + + def init_session + sid, session = store.send :get_session, env, nil + session[:key] = "stub" + return sid, session + end + + def store_session(sid, session, options = {}) + store.send :set_session, env, sid, session, options + return sid, session + end + + def expire_session(sid, session) + CouchTester.new.update sid, + "expires" => (Time.now - 10.minutes).utc.iso8601 + return sid, session + end + +end -- cgit v1.2.3