Skip to content

Commit

Permalink
KTOR-917 Keep some characters in cookie URI encoding according to RFC…
Browse files Browse the repository at this point in the history
… 3986
  • Loading branch information
e5l committed Oct 10, 2022
1 parent 5d00550 commit 9375eec
Show file tree
Hide file tree
Showing 8 changed files with 277 additions and 202 deletions.
2 changes: 1 addition & 1 deletion ktor-http/common/src/io/ktor/http/Cookie.kt
Expand Up @@ -183,7 +183,7 @@ public fun encodeCookieValue(value: String, encoding: CookieEncoding): String =
else -> value
}
CookieEncoding.BASE64_ENCODING -> value.encodeBase64()
CookieEncoding.URI_ENCODING -> value.encodeURLQueryComponent(encodeFull = true, spaceToPlus = true)
CookieEncoding.URI_ENCODING -> value.encodeURLQueryComponent(encodeFull = false, spaceToPlus = true)
}

/**
Expand Down
2 changes: 1 addition & 1 deletion ktor-http/common/test/io/ktor/tests/http/CodecTest.kt
Expand Up @@ -141,7 +141,7 @@ class CodecTest {
mapOf(
"a" to listOf("b", "c", "d"),
"1" to listOf("2"),
"x" to listOf("y", "z"),
"x" to listOf("y", "z")
).entries.formUrlEncodeTo(result)

assertEquals("a=b&a=c&a=d&1=2&x=y&x=z", result.toString())
Expand Down
@@ -0,0 +1,32 @@
/*
* Copyright 2014-2022 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
*/

package io.ktor.tests.server.plugins

import io.ktor.http.*
import kotlin.test.*

class Base64EncodingCookiesTest {
@Test
fun `no bad characters`() {
testEncode("YWJj", "abc")
}

@Test
fun `space inside`() {
testEncode("YWJjIDEyMw==", "abc 123")
}

@Test
fun `equals inside`() {
testEncode("YWJjPTEyMw==", "abc=123")
}

private fun testEncode(expected: String, value: String): String {
val encoded = encodeCookieValue(value, CookieEncoding.BASE64_ENCODING)
assertEquals(expected, encoded, "Encode failed")
assertEquals(value, decodeCookieValue(encoded, CookieEncoding.BASE64_ENCODING), "Decode failed")
return encoded
}
}
Expand Up @@ -177,203 +177,3 @@ class CookiesTest {
private fun String.cutSetCookieHeader() = substringBeforeLast("\$x-enc").trimEnd().removeSuffix(";")
}

class DQuotesEncodingTest {
@Test
fun `no bad characters`() {
testEncode("abc", "abc")
}

@Test
fun `space inside`() {
testEncode("\"abc 123\"", "abc 123")
}

@Test
fun `equals inside`() {
testEncode("abc=123", "abc=123")
}

private fun testEncode(expected: String, value: String) {
val encoded = encodeCookieValue(value, CookieEncoding.DQUOTES)
assertEquals(expected, encoded, "Encode failed")
assertEquals(value, decodeCookieValue(encoded, CookieEncoding.DQUOTES), "Decode failed")
}
}

class Base64EncodingTest {
@Test
fun `no bad characters`() {
testEncode("YWJj", "abc")
}

@Test
fun `space inside`() {
testEncode("YWJjIDEyMw==", "abc 123")
}

@Test
fun `equals inside`() {
testEncode("YWJjPTEyMw==", "abc=123")
}

private fun testEncode(expected: String, value: String): String {
val encoded = encodeCookieValue(value, CookieEncoding.BASE64_ENCODING)
assertEquals(expected, encoded, "Encode failed")
assertEquals(value, decodeCookieValue(encoded, CookieEncoding.BASE64_ENCODING), "Decode failed")
return encoded
}
}

class URIEncodingTest {
@Test
fun `no bad characters`() {
testEncode("abc", "abc")
}

@Test
fun `space inside`() {
testEncode("abc+123", "abc 123")
}

@Test
fun `equals inside`() {
testEncode("abc%3D123", "abc=123")
}

private fun testEncode(expected: String, value: String) {
val encoded = encodeCookieValue(value, CookieEncoding.URI_ENCODING)
assertEquals(expected, encoded, "Encode failed")
assertEquals(value, decodeCookieValue(encoded, CookieEncoding.URI_ENCODING), "Decode failed")
}
}

class RawCookieTest {
@Test
fun testRawEncodingWithEquals() {
val cookieValue = "my.value.key=my.value.value+my.value.id=5"
val encoded = encodeCookieValue(cookieValue, CookieEncoding.RAW)
assertEquals(cookieValue, encoded)
}
}

class ParserServerSetCookieTest {
@Test
fun testSimpleParse() {
val header = "key=value; max-Age=999; \$extension=1; \$x-enc=URI_ENCODING"
val parsed = parseServerSetCookieHeader(header)

assertEquals("key", parsed.name)
assertEquals("value", parsed.value)
assertEquals(999, parsed.maxAge)
assertEquals("1", parsed.extensions["\$extension"])
assertEquals(CookieEncoding.URI_ENCODING, parsed.encoding)
}

@Test
fun testSimpleParseCustomEncoding() {
val header = "key=value; max-Age=999; \$extension=1; \$x-enc=RAW"
val parsed = parseServerSetCookieHeader(header)

assertEquals("key", parsed.name)
assertEquals("value", parsed.value)
assertEquals(999, parsed.maxAge)
assertEquals("1", parsed.extensions["\$extension"])
assertEquals(CookieEncoding.RAW, parsed.encoding)
}

@Test
fun testSimpleParseMissingEncoding() {
val header = "key=value; max-Age=999; \$extension=1; \$x-enc=URI_ENCODING"
val parsed = parseServerSetCookieHeader(header)

assertEquals("key", parsed.name)
assertEquals("value", parsed.value)
assertEquals(999, parsed.maxAge)
assertEquals("1", parsed.extensions["\$extension"])
assertEquals(CookieEncoding.URI_ENCODING, parsed.encoding)
}

@Test
fun testSimpleParseVersionAtStart() {
val header = "\$Version=1; key=value; max-Age=999; \$extension=1; \$x-enc=URI_ENCODING"
val parsed = parseServerSetCookieHeader(header)

assertEquals("key", parsed.name)
assertEquals("value", parsed.value)
assertEquals(999, parsed.maxAge)
assertEquals("1", parsed.extensions["\$extension"])
assertEquals(CookieEncoding.URI_ENCODING, parsed.encoding)
}

@Test
fun testParseWithQuotes() {
val header = "key=\"aaa; bbb = ccc\"; max-Age=999; \$extension=1; \$x-enc=URI_ENCODING"
val parsed = parseServerSetCookieHeader(header)

assertEquals("key", parsed.name)
assertEquals("aaa; bbb = ccc", parsed.value)
assertEquals(999, parsed.maxAge)
assertEquals("1", parsed.extensions["\$extension"])
assertEquals(CookieEncoding.URI_ENCODING, parsed.encoding)
}

@Test
fun testParseExpires() {
val header = "SESSION=cart%3D%2523cl%26userId%3D%2523sid1; " +
"Expires=Sat, 16 Jan 2016 13:43:28 GMT; HttpOnly; \$x-enc=URI_ENCODING"
val parsed = parseServerSetCookieHeader(header)

val expires = parsed.expires
assertNotNull(expires)
assertEquals(2016, expires.year)
assertEquals(io.ktor.util.date.Month.JANUARY, expires.month)
assertEquals(16, expires.dayOfMonth)
}

@Test
fun testParseBase64() {
val header = "SESSION=MTIzCg==; \$x-enc=BASE64_ENCODING"
val parsed = parseServerSetCookieHeader(header)
assertEquals("123\n", parsed.value)
}

@Test
fun testMaxAge() {
val header = "key=aaa; max-age=999"
val parsed = parseServerSetCookieHeader(header)

assertEquals("key", parsed.name)
assertEquals("aaa", parsed.value)
assertEquals(999, parsed.maxAge)
}

@Test
fun testMaxAgeNegative() {
val header = "key=aaa; max-age=-1"
val parsed = parseServerSetCookieHeader(header)

assertEquals("key", parsed.name)
assertEquals("aaa", parsed.value)
assertEquals(0, parsed.maxAge)
}

@Test
fun testMaxAgeTooLong() {
val header = "key=aaa; max-age=3153600000"
val parsed = parseServerSetCookieHeader(header)

assertEquals("key", parsed.name)
assertEquals("aaa", parsed.value)
assertEquals(Int.MAX_VALUE, parsed.maxAge)
}

@Test
fun testSlash() {
val header = "384f8bdb/sessid=GLU787LwmQa9uLqnM7nWHzBm; path=/"
val parsed = parseServerSetCookieHeader(header)

assertEquals("384f8bdb/sessid", parsed.name)
assertEquals("GLU787LwmQa9uLqnM7nWHzBm", parsed.value)
assertEquals("/", parsed.path)
}
}
@@ -0,0 +1,31 @@
/*
* Copyright 2014-2022 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
*/

package io.ktor.tests.server.plugins

import io.ktor.http.*
import kotlin.test.*

class DQuotesCookiesEncodingTest {
@Test
fun `no bad characters`() {
testEncode("abc", "abc")
}

@Test
fun `space inside`() {
testEncode("\"abc 123\"", "abc 123")
}

@Test
fun `equals inside`() {
testEncode("abc=123", "abc=123")
}

private fun testEncode(expected: String, value: String) {
val encoded = encodeCookieValue(value, CookieEncoding.DQUOTES)
assertEquals(expected, encoded, "Encode failed")
assertEquals(value, decodeCookieValue(encoded, CookieEncoding.DQUOTES), "Decode failed")
}
}

0 comments on commit 9375eec

Please sign in to comment.