Skip to content

Commit

Permalink
add support for "sf" #2
Browse files Browse the repository at this point in the history
  • Loading branch information
bblfish committed Nov 8, 2022
1 parent 163aa82 commit 238d2e6
Show file tree
Hide file tree
Showing 8 changed files with 124 additions and 81 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ trait AkkaHeaderSelector[HM <: Http.Message[IO, AH]] extends HeaderSelector[HM]:
val m = msg.asInstanceOf[ak.HttpMessage]
val headersValues: Seq[String] = m.headers
.filter(_.lowercaseName() == lowercaseHeaderName)
.map(_.value())
.map(_.value().trim().nn)
headersValues match
case Seq() => Failure(UnableToCreateSigHeaderException(
s"No headers »$lowercaseHeaderName« in http message"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ package run.cosy.http.auth

import scala.util.Success
import akka.http.scaladsl.model.HttpMethods.*
import akka.http.scaladsl.model.headers.*
import akka.http.scaladsl.model.headers.{`Cache-Control`, *}
import akka.http.scaladsl.model.{
ContentTypes,
DateTime,
Expand Down Expand Up @@ -49,7 +49,6 @@ import _root_.run.cosy.http.headers.MessageSelector

import scala.language.implicitConversions
import _root_.run.cosy.akka.http.AkkaTp.HT as AH

import cats.effect.{Async, IO}

class AkkaHttpMessageSigningSuite extends HttpMessageSigningSuite[IO, AH]:
Expand All @@ -70,7 +69,7 @@ class AkkaHttpMessageSigningSuite extends HttpMessageSigningSuite[IO, AH]:
@throws[Exception]
def toRequest(request: HttpMessage): Request[IO, AH] =
request match
case `§2.1.2_HeaderField` => HttpRequest(
case `§2.1_HeaderField` => HttpRequest(
method = HttpMethods.GET,
uri = Uri("/xyz"),
headers = Seq(
Expand All @@ -90,7 +89,16 @@ class AkkaHttpMessageSigningSuite extends HttpMessageSigningSuite[IO, AH]:
`Cache-Control`(CacheDirectives.`max-age`(60)),
RawHeader("X-Empty-Header", ""),
`Cache-Control`(CacheDirectives.`must-revalidate`),
RawHeader("X-Dictionary", " a=1, b=2;x=1;y=2, c=(a b c)")
RawHeader("Example-Dict", " a=1, b=2;x=1;y=2, c=(a b c)")
)
)
case `§2.1.2_HeaderField` => HttpRequest(
method = HttpMethods.GET,
uri = Uri("/xyz"),
headers = Seq(
RawHeader("Example-Dict", " a=1, b=2;x=1;y=2, c=(a b c) "),
`Cache-Control`(CacheDirectives.`must-revalidate`),
RawHeader("Example-Dict", " d ")
)
)
case `§2.2.x_Request` => HttpRequest(
Expand Down Expand Up @@ -301,8 +309,8 @@ class AkkaHttpMessageSigningSuite extends HttpMessageSigningSuite[IO, AH]:
val `x-obs-fold-header`: MessageSelector[Request[IO, AH]] =
new UntypedAkkaSelector[Request[IO, AH]]:
override val lowercaseName: String = "x-obs-fold-header"
val `x-dictionary`: DictSelector[Message[IO, AH]] = new AkkaDictSelector[Message[IO, AH]]:
override val lowercaseName: String = "x-dictionary"
val `example-dict`: DictSelector[Message[IO, AH]] = new AkkaDictSelector[Message[IO, AH]]:
override val lowercaseName: String = "example-dict"

test("§2.2.6. Request Target for CONNECT (does not work for Akka)".fail) {
// this does not work for AKKA because akka returns //www.example.com:80
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,9 +94,9 @@ trait Http4sMessageSigningSuite[F[_]] extends HttpMessageSigningSuite[F, H4]:
new BasicHeaderSelector[F, Request[F, H4]]:
override val lowercaseName: String = "x-obs-fold-header"

override val `x-dictionary`: DictSelector[Message[F, H4]] =
override val `example-dict`: DictSelector[Message[F, H4]] =
new Http4sDictSelector[F, Message[F, H4]]:
override val lowercaseName: String = "x-dictionary"
override val lowercaseName: String = "example-dict"

test("§2.2.6. Request Target for CONNECT (works for Http4s but not Akka)") {
// this does not work for AKKA because akka returns //www.example.com:80
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@ package run.cosy.http.auth
import cats.MonadError
import cats.effect.kernel.{Clock, MonadCancel}
import cats.syntax.all.*
import run.cosy.http.{Http, HttpOps}
import run.cosy.http.auth.Agent
import run.cosy.http.headers.Rfc8941.*
import run.cosy.http.headers.{SelectorOps, *}
import _root_.run.cosy.http.{Http, HttpOps}
import _root_.run.cosy.http.auth.Agent
import _root_.run.cosy.http.headers.Rfc8941.*
import _root_.run.cosy.http.headers.{SelectorOps, *}
import scodec.bits.ByteVector

import java.nio.charset.StandardCharsets
Expand All @@ -32,7 +32,7 @@ import scala.annotation.tailrec
import scala.collection.immutable.ArraySeq
import scala.concurrent.{ExecutionContext, Future}
import scala.util.{Failure, Success, Try}
import Http.*
import _root_.run.cosy.http.Http.*

trait SignatureInputMatcher[H <: Http]:
type SI <: Header[H]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,17 @@
package run.cosy.http.headers

import cats.data.NonEmptyList
import run.cosy.http.auth.{
import _root_.run.cosy.http.auth.{
HTTPHeaderParseException,
InvalidSigException,
SelectorException,
UnableToCreateSigHeaderException
}
import run.cosy.http.headers.Rfc8941.SfDict
import _root_.run.cosy.http.headers.Rfc8941.Serialise.given
import _root_.run.cosy.http.headers.Rfc8941.SfDict

import scala.collection.immutable.ListMap
import scala.util.{Failure, Success, Try}
import run.cosy.http.headers.Rfc8941.Serialise.given

/** Header info specifying properties of a header for Signing Http Messages, and how to create them
*
Expand Down Expand Up @@ -80,44 +80,63 @@ end HeaderSelector

trait BasicMessageHeaderSelector[M] extends BasicMessageSelector[M] with HeaderSelector[M]

/** trait for headers recognised as being capable of being interpreted as a dictionary. Unless the
* right parameters are passed thought, it need not be.
*/
trait DictSelector[HM] extends MessageSelector[HM] with HeaderSelector[HM]:
val keyParam = Rfc8941.Token("key")
val sfParam = Rfc8941.Token("sf")

/** @param params
* the parameters passed to this request
* the parameters passed to this request. Can be "sf", "key", "req" or others. All parameters
* passed must continue on to the output. todo: is that correct or should one fail on all non
* undertsaod parameters?
* @return
* a signing string for the selected entry of the sf-dictionary encoded header. Fail if the
* header does not exist or is malformed
*/
override def signingString(msg: HM, params: Rfc8941.Params = ListMap()): Try[String] =
params.toSeq match
case Seq() => signingStringFor(msg)
case Seq(keyParam -> (tk: Rfc8941.SfString)) => signingStringFor(msg, tk)
case _ =>
Failure(InvalidSigException(s"""Dictionary Selector params »${params}« is malformed """))

final def signingStringFor(msg: HM): Try[String] = sfDictParse(msg).map { dict =>
headerName + dict.canon
val strVal: Try[String] =
params.collectFirst {
case (`keyParam`, name: Rfc8941.SfString) => signingValueFor(msg, name)
} getOrElse {
if params.contains(sfParam) then sfDictParse(msg).map(_.canon)
else plainSigningValueFor(msg)
}
strVal.map(v => headerName(params) + v)

final def plainSigningValueFor(msg: HM): Try[String] = filterHeaders(msg).map { nonel =>
nonel.toList.mkString(", ")
}

final def signingStringFor(msg: HM, key: Rfc8941.SfString): Try[String] =
final def signingValueFor(msg: HM, key: Rfc8941.SfString): Try[String] =
for
dict <- sfDictParse(msg)
value <- dict.get(Rfc8941.Token(key.asciiStr)).toRight(UnableToCreateSigHeaderException(
s"could not find $key in header [$dict]"
)).toTry
yield headerName(key) + value.canon
dict <- sfDictParse(msg)
token <- Try(Rfc8941.Token(key.asciiStr))
value <- dict.get(token).toRight(
UnableToCreateSigHeaderException(s"could not find $key in header [$dict]")
).toTry
yield value.canon

final def sfDictParse(msg: HM): Try[SfDict] =
for
headerValues <- filterHeaders(msg)
sfDict <- parse(SelectorOps.collate(headerValues))
yield sfDict

// note: the reason the token must be surrounded by quotes `"` is because a Token may end with `:`
final def headerName(key: Rfc8941.SfString): String =
s""""$lowercaseName";key=${key.canon}: """
final def headerName: String = s""""$lowercaseName": """
/** the header name given the params. This function can be used everywhere actually note: the
* reason the token must be surrounded by quotes `"` is because a Token may end with `:`
*/
final def headerName(params: Rfc8941.Params): String =
val attrs =
if params.isEmpty then ""
else
params.toList
.map[String]((key, value) =>
if value.isInstanceOf[Boolean] then key.canon
else key.canon + "=" + value.canon
).mkString(";", ";", "")
s""""$lowercaseName"$attrs: """

def parse(headerValue: String): Try[SfDict] =
Rfc8941.Parser.sfDictionary.parseAll(headerValue) match
Expand All @@ -138,9 +157,10 @@ object `@signature-params`:
* The Type of the HttpMessage for the platform
*/
case class SelectorOps[HM] private (selectorDB: Map[Rfc8941.SfString, MessageSelector[HM]]):
import scala.language.implicitConversions
import Rfc8941.Serialise.given

import scala.language.implicitConversions

/** add new selectors to this one */
def append(selectors: MessageSelector[HM]*): SelectorOps[HM] =
val pairs = for (selector <- selectors)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ trait HttpMessageSigningSuite[F[_], H <: Http] extends CatsEffectSuite:
val `x-empty-header`: MessageSelector[Request[F, H]]
val `x-ows-header`: MessageSelector[Request[F, H]]
val `x-obs-fold-header`: MessageSelector[Request[F, H]]
val `x-dictionary`: DictSelector[Message[F, H]]
val `example-dict`: DictSelector[Message[F, H]]

// given ec: ExecutionContext = scala.concurrent.ExecutionContext.global
given clock: Clock =
Expand All @@ -84,10 +84,10 @@ trait HttpMessageSigningSuite[F[_], H <: Http] extends CatsEffectSuite:
`x-empty-header`,
`x-ows-header`,
`x-obs-fold-header`,
`x-dictionary`
`example-dict`
)
given specialResponseSelectorOps: SelectorOps[Response[F, H]] =
selectorsSecure.responseSelectorOps.append(`x-dictionary`)
selectorsSecure.responseSelectorOps.append(`example-dict`)

@throws[Exception]
def toRequest(request: HttpMessage): Request[F, H]
Expand All @@ -101,7 +101,7 @@ trait HttpMessageSigningSuite[F[_], H <: Http] extends CatsEffectSuite:
/** example from [[https://www.ietf.org/archive/id/draft-ietf-httpbis-message-signatures-07.html
*/
// the example in the spec does not have the `GET`. Added here for coherence
val `§2.1.2_HeaderField`: HttpMessage =
val `§2.1_HeaderField`: HttpMessage =
"""GET /xyz HTTP/1.1
|Host: www.example.com
|Date: Sat, 07 Jun 2014 20:51:35 GMT
Expand All @@ -110,15 +110,15 @@ trait HttpMessageSigningSuite[F[_], H <: Http] extends CatsEffectSuite:
|X-Obs-Fold-Header: Obsolete
| line folding. \
|
|Cache-Control: max-age=60
|X-Empty-Header: \
|
|Cache-Control: max-age=60
|Cache-Control: must-revalidate
|X-Dictionary: a=1, b=2;x=1;y=2, c=(a b c)""".rfc8792single
|Example-Dict: a=1, b=2;x=1;y=2, c=(a b c)""".rfc8792single

test("§2.1.2 HTTP Field Examples") {
test("§2.1 HTTP Field Examples") {
import messageSignature.signingString
val rfcCanonReq: Request[F, H] = toRequest(`§2.1.2_HeaderField`)
val rfcCanonReq: Request[F, H] = toRequest(`§2.1_HeaderField`)
assertEquals(
`cache-control`.signingString(rfcCanonReq),
expectedHeader("cache-control", "max-age=60, must-revalidate")
Expand All @@ -141,10 +141,10 @@ trait HttpMessageSigningSuite[F[_], H <: Http] extends CatsEffectSuite:
`x-ows-header`.signingString(rfcCanonReq),
expectedHeader("x-ows-header", "Leading and trailing whitespace.")
)

assertEquals(
`x-dictionary`.signingString(rfcCanonReq),
expectedHeader("x-dictionary", "a=1, b=2;x=1;y=2, c=(a b c)")
`example-dict`.signingString(rfcCanonReq),
expectedHeader("example-dict", "a=1, b=2;x=1;y=2, c=(a b c)")
)

val il = IList(
Expand All @@ -154,7 +154,7 @@ trait HttpMessageSigningSuite[F[_], H <: Http] extends CatsEffectSuite:
`x-empty-header`.sf,
`x-obs-fold-header`.sf,
`x-ows-header`.sf,
`x-dictionary`.sf
`example-dict`.sf
)(Token("keyid") -> sf"some-key")
val Some(signatureInput) = SigInput(il): @unchecked
assertEquals(
Expand All @@ -167,9 +167,9 @@ trait HttpMessageSigningSuite[F[_], H <: Http] extends CatsEffectSuite:
|
|"x-obs-fold-header": Obsolete line folding.
|"x-ows-header": Leading and trailing whitespace.
|"x-dictionary": a=1, b=2;x=1;y=2, c=(a b c)
|"example-dict": a=1, b=2;x=1;y=2, c=(a b c)
|"@signature-params": ("cache-control" "date" "host" "x-empty-header" \
|"x-obs-fold-header" "x-ows-header" "x-dictionary");keyid="some-key"""".rfc8792single
|"x-obs-fold-header" "x-ows-header" "example-dict");keyid="some-key"""".rfc8792single
)
)

Expand All @@ -182,7 +182,7 @@ trait HttpMessageSigningSuite[F[_], H <: Http] extends CatsEffectSuite:
sf"x-not-implemented",
`x-obs-fold-header`.sf,
`x-ows-header`.sf,
`x-dictionary`.sf
`example-dict`.sf
)(Token("keyid") -> sf"some-key")
val Some(signatureInputFail) = SigInput(il2): @unchecked
rfcCanonReq.signingStr(signatureInputFail) match
Expand All @@ -203,41 +203,56 @@ trait HttpMessageSigningSuite[F[_], H <: Http] extends CatsEffectSuite:

}

test("§2.1.3 Dictionary Structured Field Members") {
test("§2.1.1 Strict Serialization of HTTP Structured Fields") {
val rfcCanonReq: Request[F, H] = toRequest(`§2.1_HeaderField`)
assertEquals(
`example-dict`.signingString(rfcCanonReq, Rfc8941.Params(`example-dict`.sfParam -> true)),
Success(""""example-dict";sf: a=1, b=2;x=1;y=2, c=(a b c)""")
)

}

val `§2.1.2_HeaderField`: HttpMessage =
"""GET /xyz HTTP/1.1
|Example-Dict: a=1, b=2;x=1;y=2, c=(a b c) \
|
|Cache-Control: must-revalidate
|Example-Dict: d \
|""".rfc8792single

test("§2.1.2 Dictionary Structured Field Members") {
import `example-dict`.{keyParam,sfParam}
import Rfc8941.Params

val rfcCanonReq: Request[F, H] = toRequest(`§2.1.2_HeaderField`)
assertEquals(
`x-dictionary`.signingStringFor(rfcCanonReq),
expectedHeader("x-dictionary", "a=1, b=2;x=1;y=2, c=(a b c)")
`example-dict`.plainSigningValueFor(rfcCanonReq),
Success( "a=1, b=2;x=1;y=2, c=(a b c), d")
)

assertEquals(
`x-dictionary`.signingString(rfcCanonReq),
expectedHeader("x-dictionary", "a=1, b=2;x=1;y=2, c=(a b c)")
)
assertEquals(
`x-dictionary`.signingString(rfcCanonReq, Rfc8941.Params()),
expectedHeader("x-dictionary", "a=1, b=2;x=1;y=2, c=(a b c)")
`example-dict`.signingString(rfcCanonReq),
expectedHeader("example-dict", "a=1, b=2;x=1;y=2, c=(a b c), d")
)
assertEquals(
`x-dictionary`.signingStringFor(rfcCanonReq, SfString("a")),
expectedKeyedHeader("x-dictionary", "a", "1")
`example-dict`.signingString(rfcCanonReq, Rfc8941.Params(sfParam -> true)),
Success(""""example-dict";sf: a=1, b=2;x=1;y=2, c=(a b c), d""")
)
assertEquals(
`x-dictionary`.signingString(
rfcCanonReq,
Rfc8941.Params(`x-dictionary`.keyParam -> SfString("a"))
),
expectedKeyedHeader("x-dictionary", "a", "1")
`example-dict`.signingString(rfcCanonReq, Params(keyParam -> SfString("a"))),
expectedKeyedHeader("example-dict", "a", "1")
)

assertEquals(
`x-dictionary`.signingStringFor(rfcCanonReq, SfString("b")),
expectedKeyedHeader("x-dictionary", "b", "2;x=1;y=2")
assertEquals(
`example-dict`.signingString(rfcCanonReq, Params(keyParam -> SfString("d"))),
expectedKeyedHeader("example-dict", "d", "?1")
)
assertEquals(
`example-dict`.signingString(rfcCanonReq,Params(keyParam -> SfString("b"))),
expectedKeyedHeader("example-dict", "b", "2;x=1;y=2")
)
assertEquals(
`x-dictionary`.signingStringFor(rfcCanonReq, SfString("c")),
expectedKeyedHeader("x-dictionary", "c", "(a b c)")
`example-dict`.signingString(rfcCanonReq, Params(keyParam -> SfString("c"))),
expectedKeyedHeader("example-dict", "c", "(a b c)")
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,6 @@ object Rfc8941:
s"error paring token $t",
s"failed at offset ${err.failedAtOffset}"
)

private[Rfc8941] def unsafeParsed(name: String) = new Token(name)
end Token

Expand Down
Loading

0 comments on commit 238d2e6

Please sign in to comment.