Class: Session

Inherits:
Object
  • Object
show all
Defined in:
lib/fresco/runtime/runtime.rb

Overview

Signed session cookie store. Wire format: ‘urlencoded_payload.hexhmac`. The payload is visible to the client (not encrypted) but the HMAC binds it to the server’s secret — modifying the payload requires forging a 32-byte SHA-256 HMAC, which is infeasible without the key.

Spinel-shape constraints:

- Custom `[]` / `[]=` on a user class collapses callers to mrb_int
  params and mismatches the underlying String/String slots. So we
  expose only named methods (`get`, `set`, `has?`); use those rather
  than `session[k]`.
- `@data` is pinned to StrStrHash via the seed-and-clear trick used
  elsewhere in this file. `clear` reseeds the same way.

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeSession

Returns a new instance of Session.



204
205
206
207
208
# File 'lib/fresco/runtime/runtime.rb', line 204

def initialize
  @data = { "__t" => "" }
  @data.delete("__t")
  @dirty = false
end

Instance Attribute Details

#dataObject (readonly)

Returns the value of attribute data.



202
203
204
# File 'lib/fresco/runtime/runtime.rb', line 202

def data
  @data
end

#dirtyObject (readonly)

Returns the value of attribute dirty.



202
203
204
# File 'lib/fresco/runtime/runtime.rb', line 202

def dirty
  @dirty
end

Class Method Details

.last_dot(s) ⇒ Object

Last ‘.` position in s, or -1 if none. Spinel doesn’t have a stdlib rindex equivalent we can rely on; the explicit walk lowers cleanly.



278
279
280
281
282
283
284
285
# File 'lib/fresco/runtime/runtime.rb', line 278

def self.last_dot(s)
  i = s.length - 1
  while i >= 0
    return i if s[i] == "."
    i -= 1
  end
  -1
end

.timing_safe_eq(a, b) ⇒ Object

Constant-time string equality. Avoids leaking the matching prefix length via early-exit timing. Spinel doesn’t have a stdlib crypto- safe compare, so we walk byte by byte.



290
291
292
293
294
295
296
297
298
299
# File 'lib/fresco/runtime/runtime.rb', line 290

def self.timing_safe_eq(a, b)
  return false if a.length != b.length
  diff = 0
  i = 0
  while i < a.length
    diff = diff | (a[i].bytes[0] ^ b[i].bytes[0])
    i += 1
  end
  diff == 0
end

Instance Method Details

#clearObject



219
220
221
222
223
# File 'lib/fresco/runtime/runtime.rb', line 219

def clear
  @data = { "__t" => "" }
  @data.delete("__t")
  @dirty = true
end

#get(k = "") ⇒ Object

‘|| “”` keeps the missing-key semantics consistent under CRuby (where Hash#[] returns nil) and Spinel (where the StrStrHash returns “” already — the coalesce is a no-op there). Callers can then write `req.session.get(“x”).length > 0` portably.



214
# File 'lib/fresco/runtime/runtime.rb', line 214

def get(k = "");    @data[k] || ""; end

#has?(k = "") ⇒ Boolean

Returns:

  • (Boolean)


216
# File 'lib/fresco/runtime/runtime.rb', line 216

def has?(k = "");   @data.key?(k);            end

#lengthObject



217
# File 'lib/fresco/runtime/runtime.rb', line 217

def length; @data.length; end

#load_from(cookie_value, secret) ⇒ Object

Verify + decode an inbound cookie value. Returns true on success (data populated), false on missing / malformed / tampered. We don’t raise so a forged cookie just looks like “no session” to the app.



228
229
230
231
232
233
234
235
236
237
238
# File 'lib/fresco/runtime/runtime.rb', line 228

def load_from(cookie_value, secret)
  return false if cookie_value.length == 0 || secret.length == 0
  dot = Session.last_dot(cookie_value)
  return false if dot < 0
  payload = cookie_value[0, dot]
  sig     = cookie_value[dot + 1, cookie_value.length - dot - 1]
  expect  = Sock.sphttp_hmac_sha256_hex(secret, payload)
  return false unless Session.timing_safe_eq(sig, expect)
  parse_payload!(payload)
  true
end

#mark_dirtyObject

Force the dirty flag from outside the class (used by flash promotion, which mutates @data directly via the reader to clear consumed entries).



304
305
306
# File 'lib/fresco/runtime/runtime.rb', line 304

def mark_dirty
  @dirty = true
end

#parse_payload!(payload) ⇒ Object

Walk a ‘k=v&k=v` payload into @data. Sibling of parse_query_string! but writes Str keys directly (no url_decode().to_sym), so it’s private to Session.



256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
# File 'lib/fresco/runtime/runtime.rb', line 256

def parse_payload!(payload)
  pairs = payload.split("&")
  i = 0
  while i < pairs.length
    pair = pairs[i]
    if pair.length > 0
      eq = str_idx(pair, "=")
      if eq < 0
        @data[url_decode(pair)] = ""
      else
        k = url_decode(pair[0, eq])
        v = url_decode(pair[eq + 1, pair.length - eq - 1])
        @data[k] = v
      end
    end
    i += 1
  end
end

#set(k = "", v = "") ⇒ Object



215
# File 'lib/fresco/runtime/runtime.rb', line 215

def set(k = "", v = ""); @data[k] = v; @dirty = true; end

Sign + serialise the current data into a cookie value. Caller decides when (typically only when @dirty).



242
243
244
245
246
247
248
249
250
251
# File 'lib/fresco/runtime/runtime.rb', line 242

def to_cookie_value(secret)
  payload = ""
  first   = true
  @data.each do |k, v|
    payload += "&" unless first
    payload += url_encode(k) + "=" + url_encode(v)
    first = false
  end
  payload + "." + Sock.sphttp_hmac_sha256_hex(secret, payload)
end