class Rack::Session::XFile

Constants

DEFAULT_OPTIONS
File
SIDCharacters
VERSION

Public Class Methods

new(app, options = {}) click to toggle source
Calls superclass method
# File lib/rack/session/xfile.rb, line 20
def initialize(app, options = {})
  super
  @mutex       = Mutex.new
  @session_dir = default_options[:session_dir]
  @ua_filter   = default_options[:user_agent_filter]
  @sid_nbytes  = default_options[:sidbits] / 8
  setup_directories
  check_permissions
end

Public Instance Methods

delete_session(req, sid, options) click to toggle source
# File lib/rack/session/xfile.rb, line 47
def delete_session(req, sid, options)
  critical_section(req) do
    File.unlink(session_file(sid)) rescue nil
    generate_sid unless options[:drop]
  end
end
find_session(req, sid) click to toggle source
# File lib/rack/session/xfile.rb, line 36
def find_session(req, sid)
  return [nil, {}] if filter_request(req, sid)

  critical_section(req) do
    unless sid and session = read_session_file(sid)
      sid, session = generate_sid, {}
    end
    [sid, session]
  end
end
session_count() click to toggle source
# File lib/rack/session/xfile.rb, line 54
def session_count
  Dir[File.join(@session_dir, '?', '*')].count
end
write_session(req, sid, session, options) click to toggle source
# File lib/rack/session/xfile.rb, line 30
def write_session(req, sid, session, options)
  critical_section(req) do
    write_session_file(sid, session)
  end
end

Private Instance Methods

check_permissions() click to toggle source

Ensure session directory has secure permissions, else raise exception.

# File lib/rack/session/xfile.rb, line 167
def check_permissions
  perms = File.stat(@session_dir).mode
  msg   = "#{self.class}: #{@session_dir} sessions directory is "

  raise Errno::EACCES, msg + 'not owned' unless File.owned? @session_dir
  raise Errno::EACCES, msg + 'not accessible'   if perms & 0700 != 0700
  raise SecurityError, msg + 'world accessible' if perms & 0007 != 0

  if perms & 070 != 0
    if File.grpowned? @session_dir
      warn msg + 'group owned/accessible'
    else
      raise SecurityError, msg + 'group accessible'
    end
  end
end
critical_section(req) { || ... } click to toggle source

Provide thread-safety for critical sections.

# File lib/rack/session/xfile.rb, line 66
def critical_section(req)
  @mutex.lock if req.multithread?
  yield
ensure
  @mutex.unlock if @mutex.locked?
end
filter_request(req, sid) click to toggle source

Skip session processing if the request matches a filter.

User agent filter

User agents that do not utilize sessions, such as bots, may be filtered.

SID filter

The sid is provided by untrusted clients. A tampered sid could result in directory traversal, pathname, or guessing exploits. Do not waste resources on a client that sends an invalid sid.

NOTE: Whitelist all valid sid characters. It's probably not sufficient to blacklist only potentially dangerous chars such as '..', '/' or ''.

sid bit range: 62^10-62^50 (approx. 2^60-2^297)

# File lib/rack/session/xfile.rb, line 137
def filter_request(req, sid)
  if @ua_filter and req.user_agent =~ @ua_filter
    return req.session.options[:skip] = true
  end

  if sid and sid !~ /^[a-zA-Z0-9]{10,50}$/
    warn "#{self.class} SID=#{sid}: tampered sid detected."
    req.session.options[:skip] = true
  end
end
generate_sid() click to toggle source

Atomically generate a unique session ID and allocate the resource file. Returns the session ID. – The sid is also a filename. Only SIDCharacters are allowed in the sid. If super is called it returns a HEX sid, which is a subset.

# File lib/rack/session/xfile.rb, line 80
def generate_sid
  sid = SecureRandom.urlsafe_base64(@sid_nbytes) rescue super
  sid.tr!('_-', SIDCharacters.sample(2).join)
  File.open(session_file(sid), File::WRONLY|File::CREAT|File::EXCL).close
  sid
rescue Errno::EEXIST
  retry
end
read_session_file(sid) click to toggle source

Read session from file using a shared lock.

# File lib/rack/session/xfile.rb, line 92
def read_session_file(sid)
  File.open(session_file(sid)) do |f|
    f.flock(File::LOCK_SH)
    begin
      f.size == 0 ? {} : Marshal.load(f)
    rescue ArgumentError, TypeError => e
      warn "#{self.class} PATH=#{session_file(sid)}: #{e.message}"
      {}
    end
  end
rescue Errno::ENOENT
end
session_file(sid) click to toggle source
# File lib/rack/session/xfile.rb, line 148
def session_file(sid)
  File.join @session_dir, sid[0], sid
end
setup_directories() click to toggle source

Create directory structure for session file distribution.

# File lib/rack/session/xfile.rb, line 155
def setup_directories
  Dir.mkdir(@session_dir, 0700) unless Dir.exist? @session_dir

  SIDCharacters.each do |char|
    dir = File.join @session_dir, char
    Dir.mkdir(dir, 0700) unless Dir.exist? dir
  end
end
write_session_file(sid, session) click to toggle source

Write session to file using an exclusive lock.

# File lib/rack/session/xfile.rb, line 108
def write_session_file(sid, session)
  File.open(session_file(sid), 'r+') do |f|
    f.flock(File::LOCK_EX)
    f.write(Marshal.dump(session))
    f.flush
    f.truncate(f.pos)
  end
  sid
rescue => e
  warn "#{self.class} cannot write session: #{e.message}"
  false
end