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

net.liftweb.http.provider.encoder.CookieEncoder.scala Maven / Gradle / Ivy

The newest version!
/*
 * Copyright 2009-2011 WorldWide Conferencing, LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package net.liftweb
package http
package provider
package encoder

import java.util._
import net.liftweb.http.provider.{HTTPCookie, SameSite}
import net.liftweb.common.{Full}

/**
  * Converts an HTTPCookie into a string to used as header cookie value.
  * 
  * The string representation follows the RFC6265
  * standard with the added field of SameSite to support secure browsers as explained at
  * MDN SameSite Cookies
  *
  * This code is based on the Netty's HTTP cookie encoder.
  *
  * Multiple cookies are supported just sending separate "Set-Cookie" headers for each cookie.
  *
  */
object CookieEncoder {

  private val VALID_COOKIE_NAME_OCTETS = validCookieNameOctets();

  private val VALID_COOKIE_VALUE_OCTETS = validCookieValueOctets();

  private val VALID_COOKIE_ATTRIBUTE_VALUE_OCTETS = validCookieAttributeValueOctets();

  private val PATH = "Path"

  private val EXPIRES = "Expires"

  private val MAX_AGE = "Max-Age"

  private val DOMAIN = "Domain"

  private val SECURE = "Secure"

  private val HTTPONLY = "HTTPOnly"

  private val SAMESITE = "SameSite"

  private val DAY_OF_WEEK_TO_SHORT_NAME = Array("Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat")

  private val CALENDAR_MONTH_TO_SHORT_NAME = Array("Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug",
                                                   "Sep", "Oct", "Nov", "Dec")

  def encode(cookie: HTTPCookie): String = {
    val name = cookie.name
    val value = cookie.value.getOrElse("")
    val skipValidation = isOldVersionCookie(cookie)
    if (!skipValidation) {
      validateCookie(name, value)
    }
    val buf = new StringBuilder()
    add(buf, name, value);
    cookie.maxAge foreach { maxAge =>
      add(buf, MAX_AGE, maxAge);
      val expires = new Date(maxAge * 1000 + System.currentTimeMillis());
      buf.append(EXPIRES);
      buf.append('=');
      appendDate(expires, buf);
      buf.append(';');
      buf.append(' ');
    }
    cookie.path foreach { path =>
      add(buf, PATH, path);
    }
    cookie.domain foreach { domain =>
      add(buf, DOMAIN, domain);
    }
    cookie.secure_? foreach { isSecure =>
      if (isSecure) add(buf, SECURE);
    }
    cookie.httpOnly foreach { isHttpOnly =>
      if (isHttpOnly) add(buf, HTTPONLY)
    }
    cookie.sameSite foreach {
      case SameSite.LAX =>
        add(buf, SAMESITE, "Lax")
      case SameSite.STRICT =>
        add(buf, SAMESITE, "Strict")
      case SameSite.NONE =>
        add(buf, SAMESITE, "None")
    }
    stripTrailingSeparator(buf)
  }

  private def validateCookie(name: String, value: String): Unit = {
    val posFirstInvalidCookieNameOctet = firstInvalidCookieNameOctet(name)
    if (posFirstInvalidCookieNameOctet >= 0) {
      throw new IllegalArgumentException("Cookie name contains an invalid char: " +
                                          name.charAt(posFirstInvalidCookieNameOctet))
    }
    val unwrappedValue = unwrapValue(value);
    if (unwrappedValue == null) {
      throw new IllegalArgumentException("Cookie value wrapping quotes are not balanced: " +
                                          value);
    }
    val postFirstInvalidCookieValueOctet = firstInvalidCookieValueOctet(unwrappedValue)
    if (postFirstInvalidCookieValueOctet >= 0) {
      throw new IllegalArgumentException("Cookie value contains an invalid char: " +
                                          unwrappedValue.charAt(postFirstInvalidCookieValueOctet));
    }
  }

  /**
    * Checks if the cookie is set with an old version 0.
    * 
    * More info about the cookie version at https://javadoc.io/static/jakarta.servlet/jakarta.servlet-api/5.0.0/jakarta/servlet/http/Cookie.html#setVersion-int-
    *
    * @param cookie
    * @return true if the cookie version is 0, false if it has no value or a different value than 0
    */
  private def isOldVersionCookie(cookie: HTTPCookie): Boolean = {
    cookie.version map (_ == 0) getOrElse false
  }

  private def appendDate(date: Date, sb: StringBuilder): StringBuilder = {
    val cal = new GregorianCalendar(TimeZone.getTimeZone("UTC"))
    cal.setTime(date)
    sb.append(DAY_OF_WEEK_TO_SHORT_NAME(cal.get(Calendar.DAY_OF_WEEK) - 1)).append(", ")
    appendZeroLeftPadded(cal.get(Calendar.DAY_OF_MONTH), sb).append(' ')
    sb.append(CALENDAR_MONTH_TO_SHORT_NAME(cal.get(Calendar.MONTH))).append(' ')
    sb.append(cal.get(Calendar.YEAR)).append(' ')
    appendZeroLeftPadded(cal.get(Calendar.HOUR_OF_DAY), sb).append(':')
    appendZeroLeftPadded(cal.get(Calendar.MINUTE), sb).append(':')
    appendZeroLeftPadded(cal.get(Calendar.SECOND), sb).append(" GMT")
  }

  private def appendZeroLeftPadded(value: Int, sb: StringBuilder): StringBuilder = {
    if (value < 10) {
      sb.append('0');
    }
    return sb.append(value);
  }

  private def validCookieNameOctets() = {
    val bits = new BitSet()
    (32 until 127) foreach bits.set
    val separators = Array('(', ')', '<', '>', '@', ',', ';', ':', '\\', '"', '/', '[', ']', '?', '=', '{',
                           '}', ' ', '\t' )
    separators.foreach(separator => bits.set(separator, false))
    bits
  }

  private def validCookieValueOctets() = {
    val bits = new BitSet()
    bits.set(0x21);
    (0x23 to 0x2B) foreach bits.set
    (0x2D to 0x3A) foreach bits.set
    (0x3C to 0x5B) foreach bits.set
    (0x5D to 0x7E) foreach bits.set
    bits
  }

  private def validCookieAttributeValueOctets() = {
    val bits = new BitSet()
    (32 until 127) foreach bits.set
    bits.set(';', false)
    bits
  }

  private def stripTrailingSeparator(buf: StringBuilder) = {
    if (buf.length() > 0) {
      buf.setLength(buf.length() - 2);
    }
    buf.toString()
  }

  private def add(sb: StringBuilder, name: String, value: Long) = {
    sb.append(name);
    sb.append('=');
    sb.append(value);
    sb.append(';');
    sb.append(' ');
  }

  private def add(sb: StringBuilder, name: String, value: String) = {
    sb.append(name);
    sb.append('=');
    sb.append(value);
    sb.append(';');
    sb.append(' ');
  }

  private def add(sb: StringBuilder, name: String) = {
    sb.append(name);
    sb.append(';');
    sb.append(' ');
  }

  private def firstInvalidCookieNameOctet(cs: CharSequence): Int = {
    return firstInvalidOctet(cs, VALID_COOKIE_NAME_OCTETS);
  }

  private def firstInvalidCookieValueOctet(cs: CharSequence): Int = {
    return firstInvalidOctet(cs, VALID_COOKIE_VALUE_OCTETS);
  }

  private def firstInvalidOctet(cs: CharSequence, bits: BitSet): Int = {
    (0 until cs.length()).foreach { i =>
      val c = cs.charAt(i)
      if (!bits.get(c)) {
      return i;
      }
    }
    -1;
  }

  private def unwrapValue(cs: CharSequence): CharSequence = {
    val len = cs.length()
    if (len > 0 && cs.charAt(0) == '"') {
      if (len >= 2 && cs.charAt(len - 1) == '"') {
        if (len == 2) "" else cs.subSequence(1, len - 1)
      } else {
        null
      }
    } else {
      cs
    }
  }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy