All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.twitter.finagle.http.headers.Rfc7230HeaderValidation.scala Maven / Gradle / Ivy

package com.twitter.finagle.http.headers

import java.util.BitSet

/**
 * Validation methods for HTTP headers.
 *
 * Methods that provide RFC-7230 (https://tools.ietf.org/html/rfc7230) header
 * validation. Invalid names or values will result in throwing an
 * `HeaderValidationException`.
 */
object Rfc7230HeaderValidation {

  /** Exception that represents header validation failure */
  sealed abstract class HeaderValidationException(details: String)
      extends IllegalArgumentException(details)

  /** Invalid header name */
  final class NameValidationException(details: String) extends HeaderValidationException(details)

  /** Invalid header value */
  final class ValueValidationException(details: String) extends HeaderValidationException(details)

  /** Validation result for header names */
  sealed trait NameValidationResult

  /** Validation result for header values */
  sealed trait ValueValidationResult

  /** Successful validation */
  case object ValidationSuccess extends NameValidationResult with ValueValidationResult

  /** Successful value validation with the detection of an obs-fold sequence */
  case object ObsFoldDetected extends ValueValidationResult

  /** Validation failed with the provided cause */
  case class ValidationFailure(ex: HeaderValidationException)
      extends NameValidationResult
      with ValueValidationResult

  private[this] def validHeaderNameChars: Iterable[Int] = {
    // https://tools.ietf.org/html/rfc7230#section-3.2.6
    //
    // token          = 1*tchar
    //
    // tchar          = "!" / "#" / "$" / "%" / "&" / "'" / "*"
    //                / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~"
    //                / DIGIT / ALPHA
    //                ; any VCHAR, except delimiters
    ("!#$%&'*+-.^_`|~".toList ++
      ('0' to '9') ++ // DIGIT
      ('a' to 'z') ++ ('A' to 'Z')).map(_.toInt) // ALPHA
  }

  private[this] def validHeaderValueChars: Iterable[Int] = {
    // Header fields: https://tools.ietf.org/html/rfc7230#section-3.2
    //
    // field-value    = *( field-content / obs-fold )
    // field-content  = field-vchar [ 1*( SP / HTAB ) field-vchar ]
    // field-vchar    = VCHAR / obs-text
    // obs-fold       = CRLF 1*( SP / HTAB )
    // VCHAR          =  %x21-7E
    //     ; visible (printing) characters, https://tools.ietf.org/html/rfc5234#appendix-B.1
    // obs-text       = %x80-FF; https://tools.ietf.org/html/rfc7230#section-3.2.6

    (0x21 to 0x7e) ++ // VCHAR
      (0x80 to 0xff) ++ // obs-text
      "\r\n \t".map(_.toInt) // Valid whitespace and obs-fold chars
  }

  private[this] val ObsFoldRegex = "\r?\n[\t ]+".r

  private[this] val validHeaderNameCharSet: java.util.BitSet = toBitSet(validHeaderNameChars)

  private[this] val validHeaderValueCharSet: java.util.BitSet = toBitSet(validHeaderValueChars)

  private[this] def toBitSet(valid: Iterable[Int]): BitSet = {
    val bitSet = new BitSet
    valid.foreach(bitSet.set)
    bitSet
  }

  private[this] def validHeaderNameChar(c: Char): Boolean = validHeaderNameCharSet.get(c)

  private[this] def validHeaderValueChar(c: Char): Boolean = validHeaderValueCharSet.get(c)

  /** Replace obs-fold sequences in the value with whitespace */
  def replaceObsFold(value: CharSequence): String = ObsFoldRegex.replaceAllIn(value, " ")

  /**
   * Validate the provided header name.
   * @param name the header name to be validated.
   */
  def validateName(name: CharSequence): NameValidationResult = {
    if (name == null) throw new NullPointerException("Header names cannot be null")
    else if (name.length == 0)
      ValidationFailure(new NameValidationException("Header name cannot be empty"))
    else {
      var i = 0
      while (i < name.length) {
        val c = name.charAt(i)
        if (!validHeaderNameChar(c))
          return ValidationFailure(
            new NameValidationException(
              s"Header '$name': name cannot contain the prohibited character '0x${Integer
                .toHexString(c)}': " + c
            ))
        i += 1
      }
      // If we made it here everything was fine.
      ValidationSuccess
    }
  }

  private[this] sealed trait ObsFoldState
  private[this] case object NonFold extends ObsFoldState
  private[this] case object LF extends ObsFoldState
  private[this] case object CR extends ObsFoldState

  /**
   * Validate the header value.
   *
   * @param name the header name. Only used for exception messages and is not validated.
   * @param value the header value to be validated.
   */
  def validateValue(name: CharSequence, value: CharSequence): ValueValidationResult = {
    if (value == null) throw new NullPointerException("Header values cannot be null")

    var i = 0
    // NonFold: Previous character was neither CR nor LF
    // CR: The previous character was CR
    // LF: The previous character was LF
    var state: ObsFoldState = NonFold
    var foldDetected = false

    while (i < value.length) {
      val c = value.charAt(i)

      if (!validHeaderValueChar(c))
        return ValidationFailure(new ValueValidationException(
          s"Header '$name': value contains a prohibited character '0x${Integer.toHexString(c)}': $c"
        ))

      state match {
        case NonFold =>
          if (c == '\r') state = CR
          else if (c == '\n') state = LF
        case CR =>
          if (c == '\n') state = LF
          else
            return ValidationFailure(
              new ValueValidationException(
                s"Header '$name': only '\\n' is allowed after '\\r' in value"))
        case LF =>
          if (c == '\t' || c == ' ') {
            foldDetected = true
            state = NonFold
          } else
            return ValidationFailure(
              new ValueValidationException(
                s"Header '$name': only ' ' and '\\t' are allowed after '\\n' in value"))
      }

      i += 1
    }

    if (state != NonFold) {
      ValidationFailure(
        new ValueValidationException(
          s"Header '$name': value must not end with '\\r' or '\\n'. Observed: " +
            (if (state == CR) "\\r" else "\\n")))
    } else if (foldDetected) {
      ObsFoldDetected
    } else {
      ValidationSuccess
    }
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy