Class: Middleware::AnonymousCache::Helper

Inherits:
Object
  • Object
show all
Defined in:
lib/middleware/anonymous_cache.rb

Overview

This gives us an API to insert anonymous cache segments

Constant Summary collapse

RACK_SESSION =
"rack.session"
USER_AGENT =
"HTTP_USER_AGENT"
ACCEPT_ENCODING =
"HTTP_ACCEPT_ENCODING"
DISCOURSE_RENDER =
"HTTP_DISCOURSE_RENDER"
REDIS_STORE_SCRIPT =
DiscourseRedis::EvalHelper.new <<~LUA
  local current = redis.call("incr", KEYS[1])
  redis.call("expire",KEYS[1],ARGV[1])
  return current
LUA
MIN_TIME_TO_CHECK =
0.05
ADP =
"action_dispatch.request.parameters"

Instance Method Summary collapse

Constructor Details

#initialize(env, request = nil) ⇒ Helper

Returns a new instance of Helper.



75
76
77
78
79
# File 'lib/middleware/anonymous_cache.rb', line 75

def initialize(env, request = nil)
  @env = env
  @user_agent = HttpUserAgentEncoder.ensure_utf8(@env[USER_AGENT])
  @request = request || Rack::Request.new(@env)
end

Instance Method Details

#blocked_crawler?Boolean

Returns:

  • (Boolean)


85
86
87
88
89
90
91
92
# File 'lib/middleware/anonymous_cache.rb', line 85

def blocked_crawler?
  @request.get? && !@request.xhr? && !@request.path.ends_with?("robots.txt") &&
    !@request.path.ends_with?("srv/status") &&
    @request[Auth::DefaultCurrentUserProvider::API_KEY].nil? &&
    @env[Auth::DefaultCurrentUserProvider::USER_API_KEY].nil? &&
    @env[Auth::DefaultCurrentUserProvider::HEADER_API_KEY].nil? &&
    CrawlerDetection.is_blocked_crawler?(crawler_identifier)
end

#cache(result, env = {}) ⇒ Object

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


307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
# File 'lib/middleware/anonymous_cache.rb', line 307

def cache(result, env = {})
  return result if GlobalSetting.anon_cache_store_threshold == 0

  status, headers, response = result

  if status == 200 && cache_duration
    if GlobalSetting.anon_cache_store_threshold > 1
      count = REDIS_STORE_SCRIPT.eval(Discourse.redis, [cache_key_count], [cache_duration])

      # technically lua will cast for us, but might as well be
      # prudent here, hence the to_i
      if count.to_i < GlobalSetting.anon_cache_store_threshold
        headers["X-Discourse-Cached"] = "skip"
        return status, headers, response
      end
    end

    headers_stripped =
      headers.dup.delete_if { |k, _| %w[Set-Cookie X-MiniProfiler-Ids].include? k }
    headers_stripped["X-Discourse-Cached"] = "true"
    parts = []
    response.each { |part| parts << part }

    if req_params = env[ADP]
      headers_stripped[ADP] = {
        "action" => req_params["action"],
        "controller" => req_params["controller"],
      }
    end

    Discourse.redis.setex(cache_key_body, cache_duration, compress(parts.join))
    Discourse.redis.setex(cache_key_other, cache_duration, [status, headers_stripped].to_json)

    headers["X-Discourse-Cached"] = "store"
  else
    parts = response
  end

  [status, headers, parts]
end

#cache_durationObject



300
301
302
# File 'lib/middleware/anonymous_cache.rb', line 300

def cache_duration
  @env["ANON_CACHE_DURATION"]
end

#cache_keyObject



160
161
162
163
164
165
166
167
168
169
170
171
172
173
# File 'lib/middleware/anonymous_cache.rb', line 160

def cache_key
  return @cache_key if defined?(@cache_key)

  # Rack `xhr?` performs a case sensitive comparison, but Rails `xhr?`
  # performs a case insensitive comparison. We use the latter everywhere
  # else in the application, so we should use it here as well.
  is_xhr = @env["HTTP_X_REQUESTED_WITH"]&.casecmp("XMLHttpRequest") == 0 ? "t" : "f"

  @cache_key =
    +"ANON_CACHE_#{is_xhr}_#{@env["HTTP_ACCEPT"]}_#{@env[Rack::RACK_URL_SCHEME]}_#{@env["HTTP_HOST"]}#{@env["REQUEST_URI"]}"

  @cache_key << AnonymousCache.build_cache_key(self)
  @cache_key
end

#cache_key_bodyObject



197
198
199
# File 'lib/middleware/anonymous_cache.rb', line 197

def cache_key_body
  @cache_key_body ||= "#{cache_key}_body"
end

#cache_key_countObject



193
194
195
# File 'lib/middleware/anonymous_cache.rb', line 193

def cache_key_count
  @cache_key_count ||= "#{cache_key}_count"
end

#cache_key_otherObject



201
202
203
# File 'lib/middleware/anonymous_cache.rb', line 201

def cache_key_other
  @cache_key_other || "#{cache_key}_other"
end

#cacheable?Boolean

Returns:

  • (Boolean)


263
264
265
266
267
268
# File 'lib/middleware/anonymous_cache.rb', line 263

def cacheable?
  !!(
    GlobalSetting.anon_cache_store_threshold > 0 && !has_auth_cookie? && get? &&
      no_cache_bypass
  )
end

#cached(env = {}) ⇒ Object



288
289
290
291
292
293
294
295
296
297
298
# File 'lib/middleware/anonymous_cache.rb', line 288

def cached(env = {})
  if body = decompress(Discourse.redis.get(cache_key_body))
    if other = Discourse.redis.get(cache_key_other)
      other = JSON.parse(other)
      if req_params = other[1].delete(ADP)
        env[ADP] = req_params
      end
      [other[0], other[1], [body]]
    end
  end
end

#check_logged_in_rate_limit!Object



244
245
246
# File 'lib/middleware/anonymous_cache.rb', line 244

def check_logged_in_rate_limit!
  !logged_in_anon_limiter.performed!(raise_error: false)
end

#clear_cacheObject



348
349
350
351
# File 'lib/middleware/anonymous_cache.rb', line 348

def clear_cache
  Discourse.redis.del(cache_key_body)
  Discourse.redis.del(cache_key_other)
end

#compress(val) ⇒ Object



270
271
272
273
274
275
276
277
# File 'lib/middleware/anonymous_cache.rb', line 270

def compress(val)
  if val && GlobalSetting.compress_anon_cache
    require "lz4-ruby" if !defined?(LZ4)
    LZ4.compress(val)
  else
    val
  end
end

#crawler_identifierObject



81
82
83
# File 'lib/middleware/anonymous_cache.rb', line 81

def crawler_identifier
  @user_agent
end

#decompress(val) ⇒ Object



279
280
281
282
283
284
285
286
# File 'lib/middleware/anonymous_cache.rb', line 279

def decompress(val)
  if val && GlobalSetting.compress_anon_cache
    require "lz4-ruby" if !defined?(LZ4)
    LZ4.uncompress(val)
  else
    val
  end
end

#force_anonymous!Object



221
222
223
224
225
226
227
228
229
230
231
232
# File 'lib/middleware/anonymous_cache.rb', line 221

def force_anonymous!
  @env[Auth::DefaultCurrentUserProvider::USER_API_KEY] = nil
  @env[Auth::DefaultCurrentUserProvider::HEADER_API_KEY] = nil
  @env["HTTP_COOKIE"] = nil
  @env["HTTP_DISCOURSE_LOGGED_IN"] = nil
  @env["rack.request.cookie.hash"] = {}
  @env["rack.request.cookie.string"] = ""
  @env["_bypass_cache"] = nil
  request = Rack::Request.new(@env)
  request.delete_param("api_username")
  request.delete_param("api_key")
end

#get?Boolean

Returns:

  • (Boolean)


205
206
207
# File 'lib/middleware/anonymous_cache.rb', line 205

def get?
  @env["REQUEST_METHOD"] == "GET"
end

#has_auth_cookie?Boolean

Returns:

  • (Boolean)


209
210
211
# File 'lib/middleware/anonymous_cache.rb', line 209

def has_auth_cookie?
  CurrentUser.has_auth_cookie?(@env)
end

#is_crawler?Boolean Also known as: key_is_crawler?

rubocop:disable Lint/BooleanSymbol

Returns:

  • (Boolean)


132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
# File 'lib/middleware/anonymous_cache.rb', line 132

def is_crawler?
  @is_crawler ||=
    begin
      if @env[DISCOURSE_RENDER] == "crawler" ||
           CrawlerDetection.crawler?(@user_agent, @env["HTTP_VIA"])
        :true
      else
        if @user_agent.downcase.include?("discourse") &&
             !@user_agent.downcase.include?("mobile")
          :true
        else
          :false
        end
      end
    end
  @is_crawler == :true
end

#is_mobile=(val) ⇒ Object

rubocop:disable Lint/BooleanSymbol



95
96
97
# File 'lib/middleware/anonymous_cache.rb', line 95

def is_mobile=(val)
  @is_mobile = val ? :true : :false
end

#is_mobile?Boolean Also known as: key_is_mobile?

Returns:

  • (Boolean)


99
100
101
102
103
104
105
106
107
108
109
110
111
# File 'lib/middleware/anonymous_cache.rb', line 99

def is_mobile?
  @is_mobile ||=
    begin
      session = @env[RACK_SESSION]
      # don't initialize params until later
      # otherwise you get a broken params on the request
      params = {}

      MobileDetection.resolve_mobile_view!(@user_agent, params, session) ? :true : :false
    end

  @is_mobile == :true
end

#key_cache_theme_idsObject



175
176
177
# File 'lib/middleware/anonymous_cache.rb', line 175

def key_cache_theme_ids
  theme_ids.join(",")
end

#key_compress_anonObject



179
180
181
# File 'lib/middleware/anonymous_cache.rb', line 179

def key_compress_anon
  GlobalSetting.compress_anon_cache
end

#key_has_brotli?Boolean

Returns:

  • (Boolean)


114
115
116
117
118
119
120
# File 'lib/middleware/anonymous_cache.rb', line 114

def key_has_brotli?
  @has_brotli ||=
    begin
      @env[ACCEPT_ENCODING].to_s =~ /br/ ? :true : :false
    end
  @has_brotli == :true
end

#key_is_modern_mobile_device?Boolean

rubocop:enable Lint/BooleanSymbol

Returns:

  • (Boolean)


152
153
154
# File 'lib/middleware/anonymous_cache.rb', line 152

def key_is_modern_mobile_device?
  MobileDetection.modern_mobile_device?(@user_agent) if @user_agent
end

#key_is_old_browser?Boolean

Returns:

  • (Boolean)


156
157
158
# File 'lib/middleware/anonymous_cache.rb', line 156

def key_is_old_browser?
  CrawlerDetection.show_browser_update?(@user_agent) if @user_agent
end

#key_localeObject

rubocop:enable Lint/BooleanSymbol



123
124
125
126
127
128
129
# File 'lib/middleware/anonymous_cache.rb', line 123

def key_locale
  if locale = Discourse.anonymous_locale(@request)
    locale
  else
    "" # No need to key, it is the same for all anon users
  end
end

#logged_in_anon_limiterObject



234
235
236
237
238
239
240
241
242
# File 'lib/middleware/anonymous_cache.rb', line 234

def logged_in_anon_limiter
  @logged_in_anon_limiter ||=
    RateLimiter.new(
      nil,
      "logged_in_anon_cache_#{@env["HTTP_HOST"]}/#{@env["REQUEST_URI"]}",
      GlobalSetting.force_anonymous_min_per_10_seconds,
      10,
    )
end

#no_cache_bypassObject



213
214
215
216
217
218
219
# File 'lib/middleware/anonymous_cache.rb', line 213

def no_cache_bypass
  request = Rack::Request.new(@env)
  request.cookies["_bypass_cache"].nil? && (request.path != "/srv/status") &&
    request[Auth::DefaultCurrentUserProvider::API_KEY].nil? &&
    @env[Auth::DefaultCurrentUserProvider::HEADER_API_KEY].nil? &&
    @env[Auth::DefaultCurrentUserProvider::USER_API_KEY].nil?
end

#should_force_anonymous?Boolean

Returns:

  • (Boolean)


251
252
253
254
255
256
257
258
259
260
261
# File 'lib/middleware/anonymous_cache.rb', line 251

def should_force_anonymous?
  if (queue_time = @env["REQUEST_QUEUE_SECONDS"]) && get?
    if queue_time > GlobalSetting.force_anonymous_min_queue_seconds
      return check_logged_in_rate_limit!
    elsif queue_time >= MIN_TIME_TO_CHECK
      return check_logged_in_rate_limit! if !logged_in_anon_limiter.can_perform?
    end
  end

  false
end

#theme_idsObject



183
184
185
186
187
188
189
190
191
# File 'lib/middleware/anonymous_cache.rb', line 183

def theme_ids
  ids, _ = @request.cookies["theme_ids"]&.split("|")
  id = ids&.split(",")&.map(&:to_i)&.first
  if id && Guardian.new.allow_themes?([id])
    Theme.transform_ids(id)
  else
    []
  end
end