2014-01-08 22:08:42 -05:00
|
|
|
require_dependency "mobile_detection"
|
2014-04-28 20:48:09 -04:00
|
|
|
require_dependency "crawler_detection"
|
2014-01-08 22:08:42 -05:00
|
|
|
|
2013-10-16 01:39:18 -04:00
|
|
|
module Middleware
|
|
|
|
class AnonymousCache
|
|
|
|
|
|
|
|
def self.anon_cache(env, duration)
|
|
|
|
env["ANON_CACHE_DURATION"] = duration
|
|
|
|
end
|
|
|
|
|
|
|
|
class Helper
|
2014-04-28 20:48:09 -04:00
|
|
|
USER_AGENT = "HTTP_USER_AGENT".freeze
|
|
|
|
RACK_SESSION = "rack.session".freeze
|
2016-12-04 21:57:09 -05:00
|
|
|
ACCEPT_ENCODING = "HTTP_ACCEPT_ENCODING".freeze
|
2014-04-28 20:48:09 -04:00
|
|
|
|
2013-10-16 01:39:18 -04:00
|
|
|
def initialize(env)
|
|
|
|
@env = env
|
|
|
|
end
|
|
|
|
|
2014-01-08 23:11:04 -05:00
|
|
|
def is_mobile=(val)
|
|
|
|
@is_mobile = val ? :true : :false
|
|
|
|
end
|
|
|
|
|
2014-01-08 22:08:42 -05:00
|
|
|
def is_mobile?
|
|
|
|
@is_mobile ||=
|
|
|
|
begin
|
2014-04-28 20:48:09 -04:00
|
|
|
session = @env[RACK_SESSION]
|
2014-01-09 00:49:12 -05:00
|
|
|
# don't initialize params until later otherwise
|
|
|
|
# you get a broken params on the request
|
|
|
|
params = {}
|
2014-04-28 20:48:09 -04:00
|
|
|
user_agent = @env[USER_AGENT]
|
2014-01-08 22:08:42 -05:00
|
|
|
|
|
|
|
MobileDetection.resolve_mobile_view!(user_agent,params,session) ? :true : :false
|
2014-04-28 20:48:09 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
@is_mobile == :true
|
|
|
|
end
|
|
|
|
|
2016-12-04 21:57:09 -05:00
|
|
|
def has_brotli?
|
|
|
|
@has_brotli ||=
|
|
|
|
begin
|
|
|
|
@env[ACCEPT_ENCODING].to_s =~ /br/ ? :true : :false
|
|
|
|
end
|
|
|
|
@has_brotli == :true
|
|
|
|
end
|
|
|
|
|
2014-04-28 20:48:09 -04:00
|
|
|
def is_crawler?
|
|
|
|
@is_crawler ||=
|
|
|
|
begin
|
|
|
|
user_agent = @env[USER_AGENT]
|
|
|
|
CrawlerDetection.crawler?(user_agent) ? :true : :false
|
|
|
|
end
|
|
|
|
@is_crawler == :true
|
2014-01-08 22:08:42 -05:00
|
|
|
end
|
|
|
|
|
2013-10-16 01:39:18 -04:00
|
|
|
def cache_key
|
2016-12-04 21:57:09 -05:00
|
|
|
@cache_key ||= "ANON_CACHE_#{@env["HTTP_ACCEPT"]}_#{@env["HTTP_HOST"]}#{@env["REQUEST_URI"]}|m=#{is_mobile?}|c=#{is_crawler?}|b=#{has_brotli?}"
|
2013-10-16 01:39:18 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
def cache_key_body
|
|
|
|
@cache_key_body ||= "#{cache_key}_body"
|
|
|
|
end
|
|
|
|
|
|
|
|
def cache_key_other
|
|
|
|
@cache_key_other || "#{cache_key}_other"
|
|
|
|
end
|
|
|
|
|
|
|
|
def get?
|
|
|
|
@env["REQUEST_METHOD"] == "GET"
|
|
|
|
end
|
|
|
|
|
2015-02-04 00:14:56 -05:00
|
|
|
def has_auth_cookie?
|
|
|
|
CurrentUser.has_auth_cookie?(@env)
|
|
|
|
end
|
|
|
|
|
2015-10-28 17:16:56 -04:00
|
|
|
def no_cache_bypass
|
|
|
|
request = Rack::Request.new(@env)
|
|
|
|
request.cookies['_bypass_cache'].nil?
|
|
|
|
end
|
|
|
|
|
2013-10-16 01:39:18 -04:00
|
|
|
def cacheable?
|
2015-10-28 17:16:56 -04:00
|
|
|
!!(!has_auth_cookie? && get? && no_cache_bypass)
|
2013-10-16 01:39:18 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
def cached
|
|
|
|
if body = $redis.get(cache_key_body)
|
|
|
|
if other = $redis.get(cache_key_other)
|
|
|
|
other = JSON.parse(other)
|
|
|
|
[other[0], other[1], [body]]
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def cache_duration
|
|
|
|
@env["ANON_CACHE_DURATION"]
|
|
|
|
end
|
|
|
|
|
|
|
|
# NOTE in an ideal world cache still serves out cached content except for one magic worker
|
|
|
|
# that fills it up, this avoids a herd killing you, we can probably do this using a job or redis tricks
|
|
|
|
# but coordinating this is tricky
|
|
|
|
def cache(result)
|
|
|
|
status,headers,response = result
|
|
|
|
|
|
|
|
if status == 200 && cache_duration
|
2014-08-14 17:54:55 -04:00
|
|
|
headers_stripped = headers.dup.delete_if{|k, _| ["Set-Cookie","X-MiniProfiler-Ids"].include? k}
|
2015-06-15 20:30:06 -04:00
|
|
|
headers_stripped["X-Discourse-Cached"] = "true"
|
2013-10-16 01:39:18 -04:00
|
|
|
parts = []
|
|
|
|
response.each do |part|
|
|
|
|
parts << part
|
|
|
|
end
|
|
|
|
|
|
|
|
$redis.setex(cache_key_body, cache_duration, parts.join)
|
|
|
|
$redis.setex(cache_key_other, cache_duration, [status,headers_stripped].to_json)
|
|
|
|
else
|
|
|
|
parts = response
|
|
|
|
end
|
|
|
|
|
|
|
|
[status,headers,parts]
|
|
|
|
end
|
|
|
|
|
|
|
|
def clear_cache
|
|
|
|
$redis.del(cache_key_body)
|
|
|
|
$redis.del(cache_key_other)
|
|
|
|
end
|
|
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
def initialize(app, settings={})
|
|
|
|
@app = app
|
|
|
|
end
|
|
|
|
|
|
|
|
def call(env)
|
|
|
|
helper = Helper.new(env)
|
|
|
|
|
|
|
|
if helper.cacheable?
|
|
|
|
helper.cached or helper.cache(@app.call(env))
|
|
|
|
else
|
|
|
|
@app.call(env)
|
|
|
|
end
|
|
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
end
|