class Rollbar::Middleware::Js

Middleware to inject the rollbar.js snippet into a 200 html response

Constants

JS_IS_INJECTED_KEY
SNIPPET

Attributes

app[R]
config[R]

Public Class Methods

new(app, config) click to toggle source
# File lib/rollbar/middleware/js.rb, line 19
def initialize(app, config)
  @app = app
  @config = config
end

Public Instance Methods

call(env) click to toggle source
# File lib/rollbar/middleware/js.rb, line 24
def call(env)
  app_result = app.call(env)

  begin
    return app_result unless add_js?(env, app_result[1])

    response_string = add_js(env, app_result[2])
    build_response(env, app_result, response_string)
  rescue StandardError => e
    Rollbar.log_error(
      "[Rollbar] Rollbar.js could not be added because #{e} exception"
    )

    app_result
  end
end

Private Instance Methods

add_js(env, response) click to toggle source
# File lib/rollbar/middleware/js.rb, line 67
def add_js(env, response)
  body = join_body(response)
  close_old_response(response)

  return nil unless body

  insert_after_idx = find_insertion_point(body)
  return nil unless insert_after_idx

  build_body_with_js(env, body, insert_after_idx)
rescue StandardError => e
  Rollbar.log_error(
    "[Rollbar] Rollbar.js could not be added because #{e} exception"
  )
  nil
end
add_js?(env, headers) click to toggle source
# File lib/rollbar/middleware/js.rb, line 47
def add_js?(env, headers)
  enabled? && !env[JS_IS_INJECTED_KEY] &&
    html?(headers) && !attachment?(headers) && !streaming?(env)
end
add_person_data(js_config, env) click to toggle source
# File lib/rollbar/middleware/js.rb, line 149
def add_person_data(js_config, env)
  person_data = extract_person_data_from_controller(env)

  return if person_data && person_data.empty?

  js_config[:payload] ||= {}
  js_config[:payload][:person] = person_data if person_data
end
attachment?(headers) click to toggle source
# File lib/rollbar/middleware/js.rb, line 56
def attachment?(headers)
  headers['Content-Disposition'].to_s.include?('attachment')
end
build_body_with_js(env, body, head_open_end) click to toggle source
# File lib/rollbar/middleware/js.rb, line 106
def build_body_with_js(env, body, head_open_end)
  return body unless head_open_end

  body[0..head_open_end] << config_js_tag(env) << snippet_js_tag(env) <<
    body[head_open_end + 1..-1]
end
build_response(env, app_result, response_string) click to toggle source
# File lib/rollbar/middleware/js.rb, line 84
def build_response(env, app_result, response_string)
  return app_result unless response_string

  env[JS_IS_INJECTED_KEY] = true

  status, headers, = app_result
  if headers.key?('Content-Length')
    headers['Content-Length'] =
      response_string.bytesize.to_s
  end

  response = ::Rack::Response.new(response_string, status, headers)

  finished = response.finish

  # Rack < 2.x Response#finish returns self in array[2].
  # Rack >= 2.x returns self.body.
  # Always return with the response object here regardless of rack version.
  finished[2] = response
  finished
end
close_old_response(response) click to toggle source
# File lib/rollbar/middleware/js.rb, line 131
def close_old_response(response)
  response.close if response.respond_to?(:close)
end
config_js_tag(env) click to toggle source
# File lib/rollbar/middleware/js.rb, line 135
def config_js_tag(env)
  require 'json'

  js_config = Rollbar::Util.deep_copy(config[:options])

  add_person_data(js_config, env)

  # MUST use the Ruby JSON encoder (JSON#generate).
  # See lib/rollbar/middleware/js/json_value
  json = ::JSON.generate(js_config)

  script_tag("var _rollbarConfig = #{json};", env)
end
enabled?() click to toggle source
# File lib/rollbar/middleware/js.rb, line 43
def enabled?
  !!config[:enabled]
end
find_end_after_regex(body, regex) click to toggle source
# File lib/rollbar/middleware/js.rb, line 119
def find_end_after_regex(body, regex)
  open_idx = body.index(regex)
  body.index('>', open_idx) if open_idx
end
find_insertion_point(body) click to toggle source
# File lib/rollbar/middleware/js.rb, line 113
def find_insertion_point(body)
  find_end_after_regex(body, /<meta\s*charset=/i) ||
    find_end_after_regex(body, /<meta\s*http-equiv="Content-Type"/i) ||
    find_end_after_regex(body, /<head\W/i)
end
html?(headers) click to toggle source
# File lib/rollbar/middleware/js.rb, line 52
def html?(headers)
  headers['Content-Type'] && headers['Content-Type'].include?('text/html')
end
html_safe_if_needed(string) click to toggle source
# File lib/rollbar/middleware/js.rb, line 178
def html_safe_if_needed(string)
  string = string.html_safe if string.respond_to?(:html_safe)
  string
end
join_body(response) click to toggle source
# File lib/rollbar/middleware/js.rb, line 124
def join_body(response)
  response.to_enum.reduce('') do |acc, fragment|
    acc << fragment.to_s
    acc
  end
end
js_snippet() click to toggle source
# File lib/rollbar/middleware/js.rb, line 162
def js_snippet
  SNIPPET
end
rails5_nonce(env) click to toggle source

Rails 5.2+ Secure Content Policy

# File lib/rollbar/middleware/js.rb, line 184
def rails5_nonce(env)
  req = ::ActionDispatch::Request.new(env)

  # Rails will only return a nonce if the app has set a nonce generator.
  # So if we get a valid nonce here, we know we should use it.
  #
  # Having both 'unsafe-inline' and a nonce is a valid and preferred
  # browser compatibility configuration.
  #
  # If the script_src key is missing, Rails will not add the nonce to the headers,
  # so we detect this and will not add it in this case.
  req.respond_to?(:content_security_policy) &&
    req.content_security_policy &&
    req.content_security_policy.directives['script-src'] &&
    req.content_security_policy_nonce
end
script_tag(content, env) click to toggle source
# File lib/rollbar/middleware/js.rb, line 166
def script_tag(content, env)
  nonce = rails5_nonce(env) || secure_headers_nonce(env)
  script_tag_content = if nonce
                         "\n<script type=\"text/javascript\" " \
                           "nonce=\"#{nonce}\">#{content}</script>"
                       else
                         "\n<script type=\"text/javascript\">#{content}</script>"
                       end

  html_safe_if_needed(script_tag_content)
end
secure_headers(req) click to toggle source
# File lib/rollbar/middleware/js.rb, line 210
def secure_headers(req)
  return SecureHeadersFalse.new unless defined?(::SecureHeaders::Configuration)

  # If the nonce key has been set, the app is using nonces for this request.
  # If it hasn't, we shouldn't cause one to be added to script_src, so return now.
  return SecureHeadersFalse.new unless secure_headers_nonce_key(req)

  config = ::SecureHeaders::Configuration

  has_nonce = ::SecureHeaders.respond_to?(
    :content_security_policy_script_nonce
  )
  secure_headers_cls = if !has_nonce
                         SecureHeadersFalse
                       elsif config.respond_to?(:get)
                         SecureHeaders3To5
                       elsif config.dup.respond_to?(:csp)
                         SecureHeaders6
                       else
                         SecureHeadersFalse
                       end

  secure_headers_cls.new
end
secure_headers_nonce(env) click to toggle source

Secure Headers gem

# File lib/rollbar/middleware/js.rb, line 202
def secure_headers_nonce(env)
  req = ::Rack::Request.new(env)

  return unless secure_headers(req).append_nonce?

  ::SecureHeaders.content_security_policy_script_nonce(req)
end
secure_headers_nonce_key(req) click to toggle source
# File lib/rollbar/middleware/js.rb, line 235
def secure_headers_nonce_key(req)
  defined?(::SecureHeaders::NONCE_KEY) &&
    req.env[::SecureHeaders::NONCE_KEY]
end
snippet_js_tag(env) click to toggle source
# File lib/rollbar/middleware/js.rb, line 158
def snippet_js_tag(env)
  script_tag(js_snippet, env)
end
streaming?(env) click to toggle source
# File lib/rollbar/middleware/js.rb, line 60
def streaming?(env)
  return false unless defined?(ActionController::Live)

  env['action_controller.instance']
    .class.included_modules.include?(ActionController::Live)
end