const { Headers } = require("../constants")
const { buildMatcherRegex, matches } = require("./matchers")

/**
 * GET, HEAD and OPTIONS methods are considered safe operations
 *
 * POST, PUT, PATCH, and DELETE methods, being state changing verbs,
 * should have a CSRF token attached to the request
 */
const EXCLUDED_METHODS = ["GET", "HEAD", "OPTIONS"]

/**
 * There are only three content type values that can be used in cross domain requests.
 * If any other value is used, e.g. application/json, the browser will first make a OPTIONS
 * request which will be protected by CORS.
 */
const INCLUDED_CONTENT_TYPES = [
  "application/x-www-form-urlencoded",
  "multipart/form-data",
  "text/plain",
]

/**
 * Validate the CSRF token generated aganst the user session.
 * Compare the token with the x-csrf-token header.
 *
 * If the token is not found within the request or the value provided
 * does not match the value within the user session, the request is rejected.
 *
 * CSRF protection provided using the 'Synchronizer Token Pattern'
 * https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#synchronizer-token-pattern
 *
 */
module.exports = (opts = { noCsrfPatterns: [] }) => {
  const noCsrfOptions = buildMatcherRegex(opts.noCsrfPatterns)
  return async (ctx, next) => {
    // don't apply for excluded paths
    const found = matches(ctx, noCsrfOptions)
    if (found) {
      return next()
    }

    // don't apply for the excluded http methods
    if (EXCLUDED_METHODS.indexOf(ctx.method) !== -1) {
      return next()
    }

    // don't apply when the content type isn't supported
    let contentType = ctx.get("content-type")
      ? ctx.get("content-type").toLowerCase()
      : ""
    if (
      !INCLUDED_CONTENT_TYPES.filter(type => contentType.includes(type)).length
    ) {
      return next()
    }

    // don't apply csrf when the internal api key has been used
    if (ctx.internal) {
      return next()
    }

    // apply csrf when there is a token in the session (new logins)
    // in future there should be a hard requirement that the token is present
    const userToken = ctx.user.csrfToken
    if (!userToken) {
      return next()
    }

    // reject if no token in request or mismatch
    const requestToken = ctx.get(Headers.CSRF_TOKEN)
    if (!requestToken || requestToken !== userToken) {
      ctx.throw(403, "Invalid CSRF token")
    }

    return next()
  }
}