@@ -51,6 +51,39 @@ def get_unviewed_ids(user)
@client.get("#{@prefix}-#{user}-v") || []
end

def flush_tokens
@client.set("#{@prefix}-tokens", nil)
end

def allowed_tokens

token_info = @client.get("#{@prefix}-tokens")
key1, key2, cycle_at = nil


if token_info
key1, key2, cycle_at = Marshal::load(token_info)

key1 = nil unless key1 && key1.length == 32
key2 = nil unless key2 && key2.length == 32

if key1 && cycle_at && (cycle_at > Time.now)
return [key1,key2].compact
end
end

timeout = Rack::MiniProfiler::AbstractStore::MAX_TOKEN_AGE

# cycle keys
key2 = key1
key1 = SecureRandom.hex
cycle_at = Time.now + timeout

@client.set("#{@prefix}-tokens", Marshal::dump([key1, key2, cycle_at]))

[key1, key2].compact
end

end
end
end
@@ -49,11 +49,15 @@ def increment_cycle
def initialize(args = nil)
args ||= {}
@expires_in_seconds = args.fetch(:expires_in) { EXPIRES_IN_SECONDS }

@token1, @token2, @cycle_tokens_at = nil

initialize_locks
initialize_cleanup_thread(args)
end

def initialize_locks
@token_lock = Mutex.new
@timer_struct_lock = Mutex.new
@user_view_lock = Mutex.new
@timer_struct_cache = {}
@@ -116,6 +120,20 @@ def cleanup_cache
@timer_struct_cache.delete_if { |k, v| v[:started] < expire_older_than }
}
end

def allowed_tokens
@token_lock.synchronize do

unless @cycle_at && (@cycle_at > Time.now)
@token2 = @token1
@token1 = SecureRandom.hex
@cycle_at = Time.now + Rack::MiniProfiler::AbstractStore::MAX_TOKEN_AGE
end

[@token1, @token2].compact

end
end
end
end
end
@@ -2,6 +2,8 @@ module Rack
class MiniProfiler
class RedisStore < AbstractStore

attr_reader :prefix

EXPIRES_IN_SECONDS = 60 * 60 * 24

def initialize(args = nil)
@@ -55,6 +57,43 @@ def diagnostics(user)
"
end

def flush_tokens
redis.del("#{@prefix}-key1", "#{@prefix}-key1_old", "#{@prefix}-key2")
end

# Only used for testing
def simulate_expire
redis.del("#{@prefix}-key1")
end

def allowed_tokens
key1, key1_old, key2 = redis.mget("#{@prefix}-key1", "#{@prefix}-key1_old", "#{@prefix}-key2")

if key1 && (key1.length == 32)
return [key1, key2].compact
end

timeout = Rack::MiniProfiler::AbstractStore::MAX_TOKEN_AGE

# TODO this could be moved to lua to correct a concurrency flaw
# it is not critical cause worse case some requests will miss profiling info

# no key so go ahead and set it
key1 = SecureRandom.hex

if key1_old && (key1_old.length == 32)
key2 = key1_old
redis.setex "#{@prefix}-key2", timeout, key2
else
key2 = nil
end

redis.setex "#{@prefix}-key1", timeout, key1
redis.setex "#{@prefix}-key1_old", timeout*2, key1

[key1, key2].compact
end

private

def redis
@@ -1,6 +1,7 @@
require 'json'
require 'timeout'
require 'thread'
require 'securerandom'

require 'mini_profiler/version'
require 'mini_profiler/asset_version'
@@ -26,12 +26,12 @@ Gem::Specification.new do |s|
s.add_development_dependency 'activerecord', '~> 3.0'
s.add_development_dependency 'dalli'
s.add_development_dependency 'rspec', '~> 2.14.1'
s.add_development_dependency 'ZenTest'
s.add_development_dependency 'autotest'
s.add_development_dependency 'redis'
s.add_development_dependency 'therubyracer'
s.add_development_dependency 'less'
s.add_development_dependency 'flamegraph'
s.add_development_dependency 'guard'
s.add_development_dependency 'guard-rspec'

s.require_paths = ["lib"]
end

This file was deleted.

@@ -30,6 +30,12 @@ def app
map '/html' do
run lambda { |env| [200, {'Content-Type' => 'text/html'}, "<html><BODY><h1>Hi</h1></BODY>\n \t</html>"] }
end
map '/whitelisted-html' do
run lambda { |env|
Rack::MiniProfiler.authorize_request
[200, {'Content-Type' => 'text/html'}, "<html><BODY><h1>Hi</h1></BODY>\n \t</html>"]
}
end
map '/implicitbody' do
run lambda { |env| [200, {'Content-Type' => 'text/html'}, "<html><h1>Hi</h1></html>"] }
end
@@ -107,7 +113,7 @@ def app

it 'avoids xss attacks' do
h = last_response.headers['X-MiniProfiler-Ids']
id = ::JSON.parse(h)[0]
_id = ::JSON.parse(h)[0]
get "/mini-profiler-resources/results?id=%22%3E%3Cqss%3E"
last_response.should_not be_ok
last_response.body.should_not =~ /<qss>/
@@ -280,13 +286,14 @@ def load_prof(response)
it "does not re-enable functionality if not whitelisted" do
Rack::MiniProfiler.config.authorization_mode = :whitelist
get '/html?pp=enable'
get '/html?pp=enable'
last_response.body.should_not include('/mini-profiler-resources/includes.js')
end

it "re-enabled functionality if whitelisted" do
Rack::MiniProfiler.config.authorization_mode = :whitelist
expect(Rack::MiniProfiler).to receive(:request_authorized?) { true }.twice
get '/html?pp=enable'
get '/whitelisted-html?pp=enable'
get '/whitelisted-html?pp=enable'
last_response.body.should include('/mini-profiler-resources/includes.js')
end
end
@@ -334,7 +341,9 @@ def load_prof(response)
end

it "should allow requests that are whitelisted" do
set_cookie("__profilin=stylin")
get '/whitelisted'
# second time will ensure cookie is set
# first time around there is no cookie, so no profiling
get '/whitelisted'
last_response.headers['X-MiniProfiler-Ids'].should_not be_nil
end
@@ -0,0 +1,77 @@
require 'spec_helper'
require 'rack'

describe Rack::MiniProfiler::ClientSettings do

describe "with settings" do
before do
@store = Rack::MiniProfiler::MemoryStore.new
settings = URI.encode_www_form_component("dp=t,bt=1")
@settings = Rack::MiniProfiler::ClientSettings.new(
{"HTTP_COOKIE" => "__profilin=#{settings};" },
@store,
Time.now
)
end

it 'has the cookies' do
@settings.has_valid_cookie?.should be_true
end

it 'has profiling disabled' do
@settings.disable_profiling?.should be_true
end

it 'has backtrace set to full' do
@settings.backtrace_full?.should be_true
end

it 'should not write cookie changes if no change' do
hash = {}
@settings.write!(hash)
hash.should == {}
end

it 'should correctly write cookie changes if changed' do
@settings.disable_profiling = false
hash = {}
@settings.write!(hash)
hash.should_not == {}
end

it 'writes auth token for authorized reqs' do
Rack::MiniProfiler.config.authorization_mode = :whitelist
Rack::MiniProfiler.authorize_request
hash = {}
@settings.write!(hash)
hash["Set-Cookie"].should include(@store.allowed_tokens.join("|"))
end

it 'does nothing on short unauthed requests' do
Rack::MiniProfiler.config.authorization_mode = :whitelist
Rack::MiniProfiler.deauthorize_request
hash = {}
@settings.handle_cookie([200, hash, []])

hash.should == {}
end

it 'discards on long unauthed requests' do
Rack::MiniProfiler.config.authorization_mode = :whitelist
Rack::MiniProfiler.deauthorize_request
hash = {}
Time.travel(Time.now + 1) do
@settings.handle_cookie([200, hash, []])
end

hash["Set-Cookie"].should include("max-age=0")
end
end

it "should not have settings by default" do
Rack::MiniProfiler::ClientSettings.new({}, Rack::MiniProfiler::MemoryStore.new, Time.now)
.has_valid_cookie?.should == false
end


end
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -9,6 +9,31 @@
@store = Rack::MiniProfiler::FileStore.new(:path => tmp)
end


describe 'allowed_tokens' do

it 'should return tokens' do
@store.flush_tokens

tokens = @store.allowed_tokens
tokens.length.should == 1
tokens.should == @store.allowed_tokens

Time.travel(Time.now + 1) do
new_tokens = @store.allowed_tokens
new_tokens.length.should == 1
new_tokens.should == tokens
end

Time.travel(Time.now + Rack::MiniProfiler::AbstractStore::MAX_TOKEN_AGE + 1) do
new_tokens = @store.allowed_tokens
new_tokens.length.should == 2
(new_tokens - tokens).length.should == 1
end

end
end

describe 'storage' do

it 'can store a PageStruct and retrieve it' do
@@ -46,6 +46,34 @@

end


describe 'allowed_tokens' do
before do
@store = Rack::MiniProfiler::MemcacheStore.new
end

it 'should return tokens' do

@store.flush_tokens

tokens = @store.allowed_tokens
tokens.length.should == 1
tokens.should == @store.allowed_tokens

Time.travel(Time.now + 1) do
new_tokens = @store.allowed_tokens
new_tokens.length.should == 1
new_tokens.should == tokens
end

Time.travel(Time.now + Rack::MiniProfiler::AbstractStore::MAX_TOKEN_AGE + 1) do
new_tokens = @store.allowed_tokens
new_tokens.length.should == 2
(new_tokens - tokens).length.should == 1
end
end
end

context 'passing in a Memcache client' do
describe 'client' do
it 'uses the passed in object rather than creating a new one' do
@@ -60,6 +60,31 @@
end
end

describe 'allowed_tokens' do
before do
@store = Rack::MiniProfiler::MemoryStore.new
end

it 'should return tokens' do

tokens = @store.allowed_tokens
tokens.length.should == 1
tokens.should == @store.allowed_tokens

Time.travel(Time.now + 1) do
new_tokens = @store.allowed_tokens
new_tokens.length.should == 1
new_tokens.should == tokens
end

Time.travel(Time.now + Rack::MiniProfiler::AbstractStore::MAX_TOKEN_AGE + 1) do
new_tokens = @store.allowed_tokens
new_tokens.length.should == 2
(new_tokens - tokens).length.should == 1
end

end
end

describe 'cache cleanup thread' do
let(:described){Rack::MiniProfiler::MemoryStore::CacheCleanupThread}
@@ -80,6 +80,27 @@

end

describe 'allowed_tokens' do
before do
@store = Rack::MiniProfiler::RedisStore.new(:db=>2)
end

it 'should return tokens' do

@store.flush_tokens

tokens = @store.allowed_tokens
tokens.length.should == 1

@store.simulate_expire

new_tokens = @store.allowed_tokens

new_tokens.length.should == 2
(new_tokens - tokens).length.should == 1
end
end


describe 'diagnostics' do
before do
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -22,6 +22,13 @@ class << self
alias_method :old_new, :new
alias_method :old_now, :now

def travel(to)
@now = to
yield
ensure
@now = nil
end

def new
@now || old_new
end