module Roda::RodaPlugins::RouteCsrf::InstanceMethods

def check_csrf!(opts=OPTS, &block)

if a block is not given, use the :csrf_failure option to determine how to handle it.
Otherwise, if a block is given, treat it as a routing block and yield to it, and
If the CSRF token is valid or the request does not require a CSRF token, return nil.
Check that the submitted CSRF token is valid, if the request requires a CSRF token.
def check_csrf!(opts=OPTS, &block)
  if msg = csrf_invalid_message(opts)
    if block
      @_request.on(&block)
    end
    
    case failure_action = opts.fetch(:csrf_failure, csrf_options[:csrf_failure])
    when :raise
      raise InvalidToken, msg
    when :empty_403
      throw :halt, [403, {'Content-Type'=>'text/html', 'Content-Length'=>'0'}, []]
    when :clear_session
      session.clear
    when :csrf_failure_method
      @_request.on{_roda_route_csrf_failure(@_request)}
    when Proc
      RodaPlugins.warn "Passing a Proc as the :csrf_failure option value to check_csrf! is deprecated"
      @_request.on{instance_exec(@_request, &failure_action)} # Deprecated
    else
      raise RodaError, "Unsupported :csrf_failure option: #{failure_action.inspect}"
    end
  end
end

def csrf_compare(s1, s2)

Perform a constant-time comparison of the two strings, returning true if they match and false otherwise.
def csrf_compare(s1, s2)
  Rack::Utils.secure_compare(s1, s2)
end

def csrf_field

for the meta tag.
The name of the hidden input tag containing the CSRF token. Also used as the name
def csrf_field
  csrf_options[:field]
end

def csrf_header

such support is enabled (it is not by default).
The HTTP header name to use when submitting CSRF tokens in an HTTP header, if
def csrf_header
  csrf_options[:header]
end

def csrf_hmac(random_data, method, path)

Return the HMAC-SHA-256 for the secret and the given arguments.
def csrf_hmac(random_data, method, path)
  OpenSSL::HMAC.digest(OpenSSL::Digest::SHA256.new, csrf_secret, "#{method.to_s.upcase}#{path}#{random_data}")
end

def csrf_invalid_message(opts)

Returns nil if the CSRF token is valid.
Returns error message string if the CSRF token is not valid.
def csrf_invalid_message(opts)
  opts = opts.empty? ? csrf_options : csrf_options.merge(opts)
  method = request.request_method
  unless opts[:check_request_methods].include?(method)
    return
  end
  unless encoded_token = opts[:token]
    encoded_token = case opts[:check_header]
    when :only
      env[opts[:env_header]]
    when true
      return (csrf_invalid_message(opts.merge(:check_header=>false)) && csrf_invalid_message(opts.merge(:check_header=>:only)))
    else
      @_request.params[opts[:field]]
    end
  end
  unless encoded_token.is_a?(String)
    return "encoded token is not a string"
  end
  if (rack_csrf_key = opts[:upgrade_from_rack_csrf_key]) && (rack_csrf_value = session[rack_csrf_key]) && csrf_compare(rack_csrf_value, encoded_token)
    return
  end
  # 31 byte random initialization vector
  # 32 byte HMAC
  # 63 bytes total
  # 84 bytes when base64 encoded
  unless encoded_token.bytesize == 84
    return "encoded token length is not 84"
  end
  begin
    submitted_hmac = Base64.strict_decode64(encoded_token)
  rescue ArgumentError
    return "encoded token is not valid base64"
  end
  random_data = submitted_hmac.slice!(0...31)
  if csrf_compare(csrf_hmac(random_data, method, @_request.path), submitted_hmac)
    return
  end
  if opts[:require_request_specific_tokens]
    "decoded token is not valid for request method and path"
  else
    unless csrf_compare(csrf_hmac(random_data, '', ''), submitted_hmac)
      "decoded token is not valid for either request method and path or for blank method and path"
    end
  end
end

def csrf_metatag

It is not recommended to use this, as it doesn't support request-specific tokens.
An HTML meta tag string containing a CSRF token that is not request-specific.
def csrf_metatag
  "<meta name=\"#{csrf_field}\" content=\"#{csrf_token}\" \/>"
end

def csrf_options

Helper for getting the plugin options.
def csrf_options
  opts[:route_csrf]
end

def csrf_path(action)

paths, URLs, empty paths).
worry about the different types of form actions (relative paths, absolute
This makes it easier to generate request-specific tokens without having to
Given a form action, return the appropriate path to use for the CSRF token.
def csrf_path(action)
  case action
  when nil, '', /\A[#?]/
    # use current path
    request.path
  when /\A(?:https?:\/)?\//
    # Either full URI or absolute path, extract just the path
    URI.parse(action).path
  else
    # relative path, join to current path
    URI.join(request.url, action).path
  end
end

def csrf_secret

JSON is used for session serialization).
secret, stored base64 encoded in the session (to handle cases where
If a secret has not already been specified, generate a random 32-byte
def csrf_secret
  key = session[csrf_options[:key]] ||= SecureRandom.base64(32)
  Base64.strict_decode64(key)
end

def csrf_tag(*args)

arguments.
An HTML hidden input tag string containing the CSRF token. See csrf_token for
def csrf_tag(*args)
  "<input type=\"hidden\" name=\"#{csrf_field}\" value=\"#{csrf_token(*args)}\" \/>"
end

def csrf_token(path=nil, method=('POST' if path))

method as the second argument.
be assumed. To generate a token for a non-POST request method, pass the
argument. By default, it a path is provided, the POST request method will
The value of the csrf token. For a path specific token, provide a path
def csrf_token(path=nil, method=('POST' if path))
  token = SecureRandom.random_bytes(31)
  token << csrf_hmac(token, method, path)
  Base64.strict_encode64(token)
end

def use_request_specific_csrf_tokens?

Whether request-specific CSRF tokens should be used by default.
def use_request_specific_csrf_tokens?
  csrf_options[:require_request_specific_tokens]
end

def valid_csrf?(opts=OPTS)

request does not require a CSRF token.
Whether the submitted CSRF token is valid for the request. True if the
def valid_csrf?(opts=OPTS)
  csrf_invalid_message(opts).nil?
end