summaryrefslogtreecommitdiffstats
path: root/lib/zorglub/session.rb
blob: 05b91d60ab4f708f11844d47faadea25fbd1956a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
require 'securerandom'

module Zorglub
  class Node
    @sessions = {}

    class << self
      attr_reader :sessions
    end

    def session
      @session ||= SessionHash.new @request, @response, Node.sessions, app.opt(:session_options)
    end
  end

  class SessionHash < Hash
    def initialize(req, resp, sessions, options)
      @request = req
      @response = resp
      @sessions = sessions
      @sid = nil
      @options = options
      super()
    end

    def [](key)
      load_data!
      super key
    end

    def key?(key)
      load_data!
      super key
    end
    alias include? key?

    def []=(key, value)
      load_data!
      super key, value
    end

    def clear
      load_data!
      # @response.delete_cookie @options[:key]
      # @sessions.delete @sid
      # @sid = nil
      super
    end

    def to_hash
      load_data!
      h = {}.replace(self)
      h.delete_if { |_k, v| v.nil? }
      h
    end

    def update(hash)
      load_data!
      super stringify_keys(hash)
    end

    def delete(key)
      load_data!
      super key
    end

    def inspect
      if loaded?
        super
      else
        "#<#{self.class}:0x#{object_id.to_s(16)} not yet loaded>"
      end
    end

    def exists?
      loaded? ? @sessions.key?(@sid) : false
    end

    def loaded?
      !@sid.nil?
    end

    def empty?
      load_data!
      super
    end

    private

    def load_data!
      return if loaded?
      return unless @options[:enabled]

      sid = @request.cookies[@options[:key]]
      if sid.nil?
        sid = generate_sid!
        @response.set_cookie @options[:key], sid
      end
      replace @sessions[sid] ||= {}
      @sessions[sid] = self
      @sid = sid
    end

    def stringify_keys(other)
      hash = {}
      other.each do |key, value|
        hash[key] = value
      end
      hash
    end

    def generate_sid!
      begin sid = sid_algorithm end while @sessions.key? sid
      sid
    end

    begin
      require 'securerandom'
      # Using SecureRandom, optional length.
      # SecureRandom is available since Ruby 1.8.7.
      # For Ruby versions earlier than that, you can require the uuidtools gem,
      # which has a drop-in replacement for SecureRandom.
      def sid_algorithm
        SecureRandom.hex(@options[:sid_len])
      end
    rescue LoadError
      require 'openssl'
      # Using OpenSSL::Random for generation, this is comparable in performance
      # with stdlib SecureRandom and also allows for optional length, it should
      # have the same behaviour as the SecureRandom::hex method of the
      # uuidtools gem.
      def sid_algorithm
        OpenSSL::Random.random_bytes(@options[:sid_len] / 2).unpack1('H*')[0]
      end
    rescue LoadError
      # Digest::SHA2::hexdigest produces a string of length 64, although
      # collisions are not very likely, the entropy is still very low and
      # length is not optional.
      #
      # Replacing it with OS-provided random data would take a lot of code and
      # won't be as cross-platform as Ruby.
      def sid_algorithm
        entropy = [srand, rand, Time.now.to_f, rand, $$, rand, object_id]
        Digest::SHA2.hexdigest(entropy.join)
      end
    end
  end
end