Class: Nyara::Session

Inherits:
ParamHash show all
Defined in:
lib/nyara/session.rb

Overview

Cookie based session
(usually it's no need to call cache or database data a "session")

Session is by default DSA + SHA2/SHA1 signed, sub config options are:

  • name - session entry name in cookie, default is 'spare_me_plz'
  • expire - expire session after seconds. default is nil, which means session expires when browser is closed
  • expires - same as expire
  • secure
    • nil(default): if request is https, add Secure option to it
    • true: always add Secure
    • false: always no Secure
  • key - DSA private key string, in der or pem format, use random if not given
  • cipher_key - if exist, use aes-256-cbc to cipher the json instead of just base64 it
    it's useful if you need to hide something but can't stop yourself from putting it into session,
    and one of the following condition matches:
    • not using http, and need to hide the info from middlemen
    • you've put something in session current_user should not see

Example

configure do set 'session', 'key', File.read(project_path 'config/session.key') set 'session', 'expire', 30 * 60 end

Please be careful with session key and cipher key, they should be separated from source code, and never shown to public.

Constant Summary collapse

CIPHER_BLOCK_SIZE =
256/8
CIPHER_RAND_MAX =
36**CIPHER_BLOCK_SIZE
JSON_DECODE_OPTS =
{create_additions: false, object_class: Session}

Class Attribute Summary collapse

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from ParamHash

#[], #[]=, #_aref, #_aset, #key?, #nested_aref, #nested_aset, parse_cookie, parse_param, split_name

Methods inherited from Hash

#to_param

Class Attribute Details

.nameObject (readonly)

Returns the value of attribute name


71
72
73
# File 'lib/nyara/session.rb', line 71

def name
  @name
end

Instance Attribute Details

#init_dataObject (readonly)

Returns the value of attribute init_data


30
31
32
# File 'lib/nyara/session.rb', line 30

def init_data
  @init_data
end

#init_digestObject (readonly)

Returns the value of attribute init_digest


30
31
32
# File 'lib/nyara/session.rb', line 30

def init_digest
  @init_digest
end

Class Method Details

.cipher(str) ⇒ Object


150
151
152
153
154
# File 'lib/nyara/session.rb', line 150

def cipher str
  iv = rand(CIPHER_RAND_MAX).to_s(36).ljust CIPHER_BLOCK_SIZE
  c = new_cipher true, iv
  encode64(iv.dup << c.update(str) << c.final)
end

.decipher(str) ⇒ Object


156
157
158
159
160
161
162
163
# File 'lib/nyara/session.rb', line 156

def decipher str
  str = decode64 str
  iv = str.byteslice 0...CIPHER_BLOCK_SIZE
  str = str.byteslice CIPHER_BLOCK_SIZE..-1
  return '' if !str or str.empty?
  c = new_cipher false, iv
  c.update(str) << c.final rescue ''
end

.decode(cookie) ⇒ Object

Decode the session hash from cookie


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
# File 'lib/nyara/session.rb', line 103

def decode cookie
  data = cookie[@name].to_s
  return empty_hash if data.empty?

  sig, str = data.split '/', 2
  return empty_hash unless str

  h = nil
  digest = nil
  begin
    sig = decode64 sig
    digest = @dss.digest str
    if @dsa.sysverify(digest, sig)
      str = @cipher_key ? decipher(str) : decode64(str)
      h = JSON.parse str, JSON_DECODE_OPTS
    end
  ensure
    return empty_hash unless h
  end

  if h.is_a?(Session)
    h.instance_variable_set :@init_digest, digest
    h.instance_variable_set :@init_data, data
    h
  else
    empty_hash
  end
end

.decode64(s) ⇒ Object


146
147
148
# File 'lib/nyara/session.rb', line 146

def decode64 s
  s.tr("-_", "+/").unpack('m0').first
end

.empty_hashObject


172
173
174
175
# File 'lib/nyara/session.rb', line 172

def empty_hash
  # todo invoke hook?
  Session.new
end

.encode(h) ⇒ Object

Encode to value

Returns

h.init_data if not changed


83
84
85
86
87
88
89
90
91
92
# File 'lib/nyara/session.rb', line 83

def encode h
  return h.init_data if h.vanila?
  str = h.to_json
  str = @cipher_key ? cipher(str) : encode64(str)
  digest = @dss.digest str
  return h.init_data if digest == h.init_digest

  sig = @dsa.syssign digest
  "#{encode64 sig}/#{str}"
end

.encode64(s) ⇒ Object

private


142
143
144
# File 'lib/nyara/session.rb', line 142

def encode64 s
  [s].pack('m0').tr("+/", "-_")
end

Encode as header line


95
96
97
98
99
100
# File 'lib/nyara/session.rb', line 95

def encode_set_cookie h, secure
  secure = @secure unless @secure.nil?
  expire = (Time.now + @expire).gmtime.rfc2822 if @expire
  # NOTE +encode h+ may return empty value, but it's still fine
  "Set-Cookie: #{@name}=#{encode h}; Path=/; HttpOnly#{'; Secure' if secure}#{"; Expires=#{expire}" if expire}\r\n"
end

Encode into a cookie hash, for test environment


74
75
76
# File 'lib/nyara/session.rb', line 74

def encode_to_cookie h, cookie
  cookie[@name] = encode h
end

.generate_cipher_keyObject


136
137
138
# File 'lib/nyara/session.rb', line 136

def generate_cipher_key
  rand(CIPHER_RAND_MAX).to_s(36).ljust CIPHER_BLOCK_SIZE
end

.generate_keyObject


132
133
134
# File 'lib/nyara/session.rb', line 132

def generate_key
  OpenSSL::PKey::DSA.generate 256
end

.initObject

Read Nyara::Config and init session settings


48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
# File 'lib/nyara/session.rb', line 48

def init
  c = Config['session'] ? Config['session'].dup : {}
  @name = Ext.escape (c.delete('name') || 'spare_me_plz').to_s, false

  if c['key']
    @dsa = OpenSSL::PKey::DSA.new c.delete 'key'
  else
    @dsa = generate_key
  end

  # DSA can sign on any digest since 1.0.0
  @dss = OpenSSL::VERSION >= '1.0.0' ? OpenSSL::Digest::SHA256 : OpenSSL::Digest::DSS1

  @cipher_key = pad_256_bit c.delete 'cipher_key'

  @expire = c.delete('expire') || c.delete('expires')
  @secure = c.delete('secure')

  unless c.empty?
    raise "unknown options in Nyara::Config[:session]: #{c.inspect}"
  end
end

.new_cipher(encrypt, iv) ⇒ Object


177
178
179
180
181
182
183
# File 'lib/nyara/session.rb', line 177

def new_cipher encrypt, iv
  c = OpenSSL::Cipher.new 'aes-256-cbc'
  encrypt ? c.encrypt : c.decrypt
  c.key = @cipher_key
  c.iv = iv
  c
end

.pad_256_bit(s) ⇒ Object


165
166
167
168
169
170
# File 'lib/nyara/session.rb', line 165

def pad_256_bit s
  s = s.to_s
  return nil if s.empty?
  len = CIPHER_BLOCK_SIZE
  s[0...len].ljust len, '*'
end

Instance Method Details

#vanila?Boolean

Returns

If the session is init with nothing, and flash is clear

Returns:

  • (Boolean)

35
36
37
38
39
# File 'lib/nyara/session.rb', line 35

def vanila?
  if @init_digest.nil?
    empty? or size == 1 && has_key?('flash.next') && self['flash.next'].empty?
  end
end