diff --git a/akka/src/main/scala/run/cosy/akka/http/headers/Signature-Input.scala b/akka/src/main/scala/run/cosy/akka/http/headers/Signature-Input.scala index 4a6964a..1551a0f 100644 --- a/akka/src/main/scala/run/cosy/akka/http/headers/Signature-Input.scala +++ b/akka/src/main/scala/run/cosy/akka/http/headers/Signature-Input.scala @@ -38,7 +38,12 @@ import cats.parse.Parser import run.cosy.akka.http.AkkaTp import run.cosy.akka.http.headers.Encoding.UnicodeString import run.cosy.akka.http.headers.{BetterCustomHeader, BetterCustomHeaderCompanion} -import run.cosy.http.auth.{HTTPHeaderParseException, InvalidSigException, SignatureInputMatcher} +import run.cosy.http.auth.{ + HTTPHeaderParseException, + InvalidSigException, + ParsingExc, + SignatureInputMatcher +} import run.cosy.http.headers.* import run.cosy.http.headers.Rfc8941.{ IList, @@ -89,12 +94,12 @@ object `Signature-Input` parse(h.value).toOption case _ => None - def parse(value: String): Try[SigInputs] = + def parse(value: String): Either[ParsingExc, SigInputs] = Rfc8941.Parser.sfDictionary.parseAll(value) match - case Left(e) => Failure(HTTPHeaderParseException(e, value)) + case Left(e) => Left(HTTPHeaderParseException(e, value)) case Right(lm) => SigInputs(lm).toRight { InvalidSigException( "Signature-Input Header Parses but data structure is not appropriate" ) - }.toTry + } end `Signature-Input` diff --git a/akka/src/main/scala/run/cosy/akka/http/headers/Signature.scala b/akka/src/main/scala/run/cosy/akka/http/headers/Signature.scala index b72fcd3..dead8f6 100644 --- a/akka/src/main/scala/run/cosy/akka/http/headers/Signature.scala +++ b/akka/src/main/scala/run/cosy/akka/http/headers/Signature.scala @@ -21,7 +21,12 @@ import akka.http.scaladsl.model.{HttpHeader, ParsingException} import run.cosy.akka.http.AkkaTp import run.cosy.akka.http.headers.{BetterCustomHeader, BetterCustomHeaderCompanion, Signature} import run.cosy.http.Http.Header -import run.cosy.http.auth.{HTTPHeaderParseException, InvalidSigException, SignatureMatcher} +import run.cosy.http.auth.{ + HTTPHeaderParseException, + InvalidSigException, + ParsingExc, + SignatureMatcher +} import run.cosy.http.headers import run.cosy.http.headers.Rfc8941.{Bytes, IList, PItem, SfDict} import run.cosy.http.headers.{Rfc8941, Signatures} @@ -55,10 +60,10 @@ object Signature case _: (RawHeader | CustomHeader) if h.lowercaseName == lowercaseName => parse(h.value).toOption case _ => None - def parse(value: String): Try[Signatures] = + def parse(value: String): Either[ParsingExc, Signatures] = Rfc8941.Parser.sfDictionary.parseAll(value) match - case Left(e) => Failure(HTTPHeaderParseException(e, value)) + case Left(e) => Left(HTTPHeaderParseException(e, value)) case Right(lm) => Signatures(lm).toRight( InvalidSigException("Signature Header Parses but data structure is not appropriate") - ).toTry + ) end Signature diff --git a/akka/src/main/scala/run/cosy/akka/http/messages/SelectorFnsAkka.scala b/akka/src/main/scala/run/cosy/akka/http/messages/SelectorFnsAkka.scala index 7ac7d8c..24213e5 100644 --- a/akka/src/main/scala/run/cosy/akka/http/messages/SelectorFnsAkka.scala +++ b/akka/src/main/scala/run/cosy/akka/http/messages/SelectorFnsAkka.scala @@ -16,14 +16,21 @@ package run.cosy.akka.http.messages -import akka.http.scaladsl.model.{HttpMessage, HttpRequest, HttpResponse} +import akka.http.scaladsl.model.* +import akka.http.scaladsl.model.headers.Host +import akka.http.scaladsl.settings.ParserSettings.ConflictingContentTypeHeaderProcessingMode.NoContentType import cats.Id import cats.data.NonEmptyList import run.cosy.akka.http.AkkaTp import run.cosy.akka.http.AkkaTp.HT import run.cosy.http.Http import run.cosy.http.Http.{Request, Response} -import run.cosy.http.auth.{AttributeException, HTTPHeaderParseException, SelectorException} +import run.cosy.http.auth.{ + AttributeException, + HTTPHeaderParseException, + ParsingExc, + SelectorException +} import run.cosy.http.headers.Rfc8941.{Param, Params} import run.cosy.http.headers.{ParsingException, Rfc8941} import run.cosy.http.messages.* @@ -31,28 +38,32 @@ import run.cosy.http.messages.* import java.nio.charset.StandardCharsets import scala.collection.immutable.ListMap import scala.util.{Failure, Success, Try} -import akka.http.scaladsl.model.headers.Host -import akka.http.scaladsl.model.HttpHeader -import akka.http.scaladsl.settings.ParserSettings.ConflictingContentTypeHeaderProcessingMode.NoContentType class SelectorFnsAkka(using sc: ServerContext) extends SelectorFns[Id, HT]: override val method: RequestFn = - RequestAkka { req => Success(req.method.value) } + RequestAkka { req => Right(req.method.value) } override val authority: RequestFn = RequestAkka { req => - Try( - req.effectiveUri(sc.secure, sc.defaultHost.map(Host(_)).getOrElse(Host.empty)) - .authority.toString().toLowerCase(java.util.Locale.ROOT).nn - ) + try + Right(req.effectiveUri( + sc.secure, + sc.defaultHost.map(Host(_)) + .getOrElse(Host.empty) + ).authority.toString().toLowerCase(java.util.Locale.ROOT).nn) + catch + case urlEx: IllegalUriException => + Left( + SelectorException("cannot calculate effectuve url for request. " + urlEx.getMessage) + ) } /** best not used if not HTTP1.1 according to spec. Does not even work with Akka (see test suite) */ override val requestTarget: RequestFn = - RequestAkka { req => Success(req.uri.toString()) } + RequestAkka { req => Right(req.uri.toString()) } // raw headers, no interpretation override def requestHeaders(headerName: HeaderId): RequestFn = @@ -68,12 +79,12 @@ class SelectorFnsAkka(using sc: ServerContext) override val path: RequestFn = RequestAkka { req => - Success(req.uri.path.toString()) + Right(req.uri.path.toString()) } override val query: RequestFn = RequestAkka { req => - Try( + Right( req.uri.queryString(StandardCharsets.US_ASCII.nn) .map("?" + _).getOrElse("?") ) @@ -82,41 +93,43 @@ class SelectorFnsAkka(using sc: ServerContext) override def queryParam(name: Rfc8941.SfString): RequestFn = RequestAkka { req => req.uri.query().getAll(name.asciiStr).reverse match - case Nil => Failure(SelectorException( + case Nil => Left(SelectorException( s"No query parameter with key ${name} found. Suspicious." )) - case head :: tail => Success(NonEmptyList(head, tail)) + case head :: tail => Right(NonEmptyList(head, tail)) } override val scheme: RequestFn = RequestAkka { req => - Try(req.effectiveUri( + Right(req.effectiveUri( securedConnection = sc.secure, defaultHostHeader = sc.defaultHost.map(Host(_)).getOrElse(Host.empty) ).scheme) } override val targetUri: RequestFn = RequestAkka { req => - Success(req.effectiveUri( + Right(req.effectiveUri( securedConnection = sc.secure, defaultHostHeader = sc.defaultHost.map(Host(_)).getOrElse(Host.empty) ).toString()) } override val status: ResponseFn = ResponseAkka { resp => - Success("" + resp.status.intValue) + Right("" + resp.status.intValue) } case class RequestAkka( - val sigValues: HttpRequest => Try[String | NonEmptyList[String]] + val sigValues: HttpRequest => Either[ParsingExc, String | NonEmptyList[String]] ) extends SelectorFn[Http.Request[Id, HT]]: - override val signingValues: Request[Id, HT] => Try[String | NonEmptyList[String]] = + override val signingValues + : Request[Id, HT] => Either[ParsingExc, String | NonEmptyList[String]] = msg => sigValues(msg.asInstanceOf[HttpRequest]) case class ResponseAkka( - val sigValues: HttpResponse => Try[String | NonEmptyList[String]] + val sigValues: HttpResponse => Either[ParsingExc, String | NonEmptyList[String]] ) extends SelectorFn[Http.Response[Id, HT]]: - override val signingValues: Response[Id, HT] => Try[String | NonEmptyList[String]] = + override val signingValues + : Response[Id, HT] => Either[ParsingExc, String | NonEmptyList[String]] = msg => sigValues(msg.asInstanceOf[HttpResponse]) end SelectorFnsAkka @@ -124,17 +137,17 @@ end SelectorFnsAkka object SelectorAkka: import run.cosy.http.headers.Rfc8941.Serialise.given - def getHeaders(name: HeaderId)(msg: HttpMessage): Try[NonEmptyList[String]] = + def getHeaders(name: HeaderId)(msg: HttpMessage): Either[ParsingExc, NonEmptyList[String]] = val N = name.specName msg.headers.collect { case HttpHeader(N, value) => value.trim.nn }.toList match case Nil => name.specName match case "content-length" => msg.entity.contentLengthOption .toRight(SelectorException("no content-length header set")) - .toTry.map(l => NonEmptyList.one("" + l)) + .map(l => NonEmptyList.one("" + l)) case "content-type" if msg.entity.contentType != NoContentType => - Success(NonEmptyList.one(msg.entity.contentType.value)) + Right(NonEmptyList.one(msg.entity.contentType.value)) case _ => - Failure(SelectorException(s"No headers named ${name.canon} selectable in request")) - case head :: tail => Success(NonEmptyList(head, tail)) + Left(SelectorException(s"No headers named ${name.canon} selectable in request")) + case head :: tail => Right(NonEmptyList(head, tail)) end SelectorAkka diff --git a/akka/src/test/scala/run/cosy/http/messages/AkkaReqSigSuite.scala b/akka/src/test/scala/run/cosy/http/messages/AkkaReqSigSuite.scala index 6ef0675..a889d5e 100644 --- a/akka/src/test/scala/run/cosy/http/messages/AkkaReqSigSuite.scala +++ b/akka/src/test/scala/run/cosy/http/messages/AkkaReqSigSuite.scala @@ -24,7 +24,7 @@ import run.cosy.http.Http given ServerContext = ServerContext("bblfish.net", true) val xxxx = new run.cosy.akka.http.messages.SelectorFnsAkka(using ServerContext("bblfish.net", true)) -class AkkaReqSigSuite extends RequestSigSuite[cats.Id, HT]( +class AkkaReqSigSuite extends SigInputReqSuite[cats.Id, HT]( run.cosy.http.auth.AkkaHttpMessageSignature, new run.cosy.http.messages.RequestSelectorDB( new AtSelectors[cats.Id, HT](using xxxx) {}, diff --git a/http4s/shared/src/main/scala/run/cosy/http4s/messages/SelectorFnsH4.scala b/http4s/shared/src/main/scala/run/cosy/http4s/messages/SelectorFnsH4.scala index af7457d..68241c7 100644 --- a/http4s/shared/src/main/scala/run/cosy/http4s/messages/SelectorFnsH4.scala +++ b/http4s/shared/src/main/scala/run/cosy/http4s/messages/SelectorFnsH4.scala @@ -25,7 +25,7 @@ import run.cosy.http.messages.Parameters.nameTk import run.cosy.http4s.Http4sTp.HT as H4 import run.cosy.platform import run.cosy.http.headers.Rfc8941 -import run.cosy.http.auth.{AttributeException, SelectorException} +import run.cosy.http.auth.{AttributeException, ParsingExc, SelectorException} import run.cosy.http4s.messages.SelectorFnsH4.getHeaders import scala.util.Try @@ -33,26 +33,25 @@ import scala.util.{Failure, Success} import org.typelevel.ci.CIString class SelectorFnsH4[F[_]](using sc: ServerContext) extends SelectorFns[F, H4]: - import SelectorFnsH4 as SF + val SF = SelectorFnsH4 override def method: RequestFn = - RequestSelH4(req => Success(req.method.name)) + RequestSelH4(req => Right(req.method.name)) override def authority: RequestFn = RequestSelH4(req => for auth <- SF.authorityFor(req) .toRight(SelectorException("could not construct authority for request")) - .toTry yield platform.StringUtil.toLowerCaseInsensitive(auth.renderString) ) /** best not used if not HTTP1.1 */ - override def requestTarget: RequestFn = RequestSelH4(req => Success(req.uri.renderString)) + override def requestTarget: RequestFn = RequestSelH4(req => Right(req.uri.renderString)) - override def path: RequestFn = RequestSelH4(req => Success(req.uri.path.toString())) + override def path: RequestFn = RequestSelH4(req => Right(req.uri.path.toString())) override def query: RequestFn = RequestSelH4(req => - Success { + Right { val q: Query = req.uri.query if q == Query.empty then "?" else if q == Query.blank then "?" @@ -63,14 +62,12 @@ class SelectorFnsH4[F[_]](using sc: ServerContext) extends SelectorFns[F, H4]: ) override def queryParam(name: Rfc8941.SfString): RequestFn = RequestSelH4(req => - Try { - req.uri.query.multiParams.get(name.asciiStr) match - case None => throw SelectorException( - s"No query parameter with key ${name.asciiStr} found. Suspicious." - ) - case Some(Nil) => "" - case Some(head :: tail) => NonEmptyList(head, tail) - } + req.uri.query.multiParams.get(name.asciiStr) match + case None => Left(SelectorException( + s"No query parameter with key ${name.asciiStr} found. Suspicious." + )) + case Some(Nil) => Right("") + case Some(head :: tail) => Right(NonEmptyList(head, tail)) ) override def scheme: RequestFn = RequestSelH4(req => @@ -85,7 +82,7 @@ class SelectorFnsH4[F[_]](using sc: ServerContext) extends SelectorFns[F, H4]: ) ) - override def status: ResponseFn = ResponseSelH4(req => Success("" + req.status.code)) + override def status: ResponseFn = ResponseSelH4(req => Right("" + req.status.code)) override def requestHeaders(name: HeaderId): RequestFn = RequestSelH4(req => SF.getHeaders(req, name) @@ -96,15 +93,17 @@ class SelectorFnsH4[F[_]](using sc: ServerContext) extends SelectorFns[F, H4]: ) case class RequestSelH4( - val sigValues: H4Request[F] => Try[String | NonEmptyList[String]] + val sigValues: H4Request[F] => Either[ParsingExc, String | NonEmptyList[String]] ) extends SelectorFn[Http.Request[F, H4]]: - override val signingValues: Http.Request[F, H4] => Try[String | NonEmptyList[String]] = + override val signingValues + : Http.Request[F, H4] => Either[ParsingExc, String | NonEmptyList[String]] = msg => sigValues(msg.asInstanceOf[H4Request[F]]) case class ResponseSelH4( - sigValues: H4Response[F] => Try[String | NonEmptyList[String]] + sigValues: H4Response[F] => Either[ParsingExc, String | NonEmptyList[String]] ) extends SelectorFn[Http.Response[F, H4]]: - override val signingValues: Http.Response[F, H4] => Try[String | NonEmptyList[String]] = + override val signingValues + : Http.Response[F, H4] => Either[ParsingExc, String | NonEmptyList[String]] = msg => sigValues(msg.asInstanceOf[H4Response[F]]) end SelectorFnsH4 @@ -113,8 +112,8 @@ object SelectorFnsH4: def getHeaders[F[_]](msg: H4Message[F], name: HeaderId) = msg.headers.get(CIString(name.specName)).map(_.map(_.value)) match - case None => Failure(SelectorException(s"no header in request named $name")) - case Some(nel) => Success(nel) + case None => Left(SelectorException(s"no header in request named $name")) + case Some(nel) => Right(nel) def defaultAuthorityOpt(scheme: Option[Uri.Scheme])( using sc: ServerContext @@ -149,16 +148,18 @@ object SelectorFnsH4: def defaultScheme(using sc: ServerContext): Uri.Scheme = if sc.secure then Uri.Scheme.https else Uri.Scheme.http - def effectiveUriFor[F[_]](req: H4Request[F])(using sc: ServerContext): Try[Uri] = + def effectiveUriFor[F[_]](req: H4Request[F])(using + sc: ServerContext + ): Either[SelectorException, Uri] = val uri = req.uri if uri.scheme.isDefined && uri.authority.isDefined - then Success(uri) + then Right(uri) else val newUri = uri.copy( scheme = uri.scheme.orElse(Some(defaultScheme)), authority = authorityFor(req) ) if newUri.authority.isDefined - then Success(newUri) - else Failure(SelectorException(s"cannot create effective Uri for req. Got: <$newUri>")) + then Right(newUri) + else Left(SelectorException(s"cannot create effective Uri for req. Got: <$newUri>")) end effectiveUriFor diff --git a/ietfSig/shared/src/main/scala/run/cosy/http/auth/AuthException.scala b/ietfSig/shared/src/main/scala/run/cosy/http/auth/AuthException.scala index df189e9..d45471a 100644 --- a/ietfSig/shared/src/main/scala/run/cosy/http/auth/AuthException.scala +++ b/ietfSig/shared/src/main/scala/run/cosy/http/auth/AuthException.scala @@ -18,19 +18,25 @@ package run.cosy.http.auth import run.cosy.http.headers.RFC8941Exception +import java.nio.charset.CharacterCodingException + //used to be ResponseSummary(on: Uri, code: StatusCode, header: Seq[HttpHeader], respTp: ContentType) //but it would be complicated to adapt for akka and http4s types case class ResponseSummary(onUri: String, code: String, header: Seq[String], respTp: String) -class AuthExc(msg: String) extends Throwable(msg, null, true, false) +sealed class AuthExc(msg: String) extends Throwable(msg, null, true, false) case class CryptoException(msg: String) extends AuthExc(msg) case class AuthException(response: ResponseSummary, msg: String) extends AuthExc(msg) case class InvalidCreatedFieldException(msg: String) extends AuthExc(msg) case class InvalidExpiresFieldException(msg: String) extends AuthExc(msg) case class UnableToCreateSigHeaderException(msg: String) extends AuthExc(msg) -case class SelectorException(msg: String) extends AuthExc(msg) -case class InvalidSigException(msg: String) extends AuthExc(msg) -case class KeyIdException(msg: String) extends AuthExc(msg) -case class AttributeException(msg: String) extends AuthExc(msg) + +case class KeyIdException(msg: String) extends AuthExc(msg) + +sealed class ParsingExc(msg: String) extends AuthExc(msg) +case class InvalidSigException(msg: String) extends ParsingExc(msg) +case class SelectorException(msg: String) extends ParsingExc(msg) +case class AttributeException(msg: String) extends ParsingExc(msg) +case class CharacterCodingExc(msg: String) extends ParsingExc(msg) case class HTTPHeaderParseException(error: cats.parse.Parser.Error, httpHeader: String) - extends AuthExc(httpHeader) + extends ParsingExc(httpHeader) diff --git a/ietfSig/shared/src/main/scala/run/cosy/http/auth/MessageSignature.scala b/ietfSig/shared/src/main/scala/run/cosy/http/auth/MessageSignature.scala index 27d5de1..3801e94 100644 --- a/ietfSig/shared/src/main/scala/run/cosy/http/auth/MessageSignature.scala +++ b/ietfSig/shared/src/main/scala/run/cosy/http/auth/MessageSignature.scala @@ -18,14 +18,14 @@ package run.cosy.http.auth import _root_.run.cosy.http.Http.* import _root_.run.cosy.http.auth.Agent -import _root_.run.cosy.http.headers.Rfc8941.* import _root_.run.cosy.http.headers.* +import _root_.run.cosy.http.headers.Rfc8941.* +import _root_.run.cosy.http.messages.{RequestSelector, RequestSelectorDB, `@signature-params`} import _root_.run.cosy.http.{Http, HttpOps} import cats.MonadError import cats.data.NonEmptyList import cats.effect.kernel.{Clock, MonadCancel} import cats.syntax.all.* -import _root_.run.cosy.http.messages.{RequestSelectorDB, `@signature-params`} import scodec.bits.ByteVector import java.nio.charset.StandardCharsets @@ -121,18 +121,32 @@ trait MessageSignature[F[_], H <: Http](using ops: HttpOps[H]): * to be added to the Request. In the latter case use the withSigInput method. todo: it may * be more correct if the result is a byte array, rather than a Unicode String. */ - def signingStr(sigInput: SigInput): Try[SigningString] = - val xl: Either[Throwable, List[String]] = sigInput.headerItems.reverse - .foldM(List(`@signature-params`.signingStr(sigInput))) { (lst, pih) => - (for - selector <- selectorDB.get(pih.item.asciiStr, pih.params) - str <- selector.signingStr(req) - yield str :: lst).toEither + def signatureBase(sigInput: SigInput): Either[ParsingExc, SigningString] = + val xl: Either[ParsingExc, List[RequestSelector[F, H]]] = sigInput.headerItems + .foldLeftM(List[RequestSelector[F, H]]()) { (lst, pih) => + selectorDB.get(pih.item.asciiStr, pih.params).map(_ :: lst) } - xl.flatMap(list => - ByteVector.encodeAscii(list.mkString("\n")) - ).toTry - end signingStr + for + list <- xl + baseList <- sigBase(list, true) + bytes <- ByteVector.encodeAscii( + baseList.mkString("\n") + `@signature-params`.paramStr(sigInput) + ).leftMap(ce => CharacterCodingExc(ce.getMessage.nn)) + yield bytes + end signatureBase + + /** return the sigbase but without the attributes, which can be appended later */ + def sigBase( + selectors: List[RequestSelector[F, H]], + reversed: Boolean = false + ): Either[ParsingExc, List[String]] = + val s = if reversed then selectors else selectors.reverse + val sp = if reversed then selectors.reverse else s + val sigParams = + s""""@signature-params": (${sp.map(s => s.identifier).mkString(" ")})""" + s.foldM(List(sigParams)) { (lst, selector) => + selector.signingStr(req).map(_ :: lst) + } extension (req: Http.Request[F, H]) /** get the signature data for a given signature name eg `sig1` from the headers. diff --git a/ietfSig/shared/src/main/scala/run/cosy/http/headers/SigInput.scala b/ietfSig/shared/src/main/scala/run/cosy/http/headers/SigInput.scala index 7fdcabf..3b64e84 100644 --- a/ietfSig/shared/src/main/scala/run/cosy/http/headers/SigInput.scala +++ b/ietfSig/shared/src/main/scala/run/cosy/http/headers/SigInput.scala @@ -101,7 +101,7 @@ final case class SigInput private (val il: IList): object SigInput: /** registered metadata parameters for Signature specifications as per - * [[https://www.ietf.org/archive/id/draft-ietf-httpbis-message-signatures-07.html#name-initial-contents-2 §6.2.2 of 07 spec]]. + * [[https://www.ietf.org/archive/id/draft-ietf-httpbis-message-signatures-15.html#name-initial-contents-2 §6.3.2 of 15 spec]]. */ val algTk = Token("alg") val createdTk = Token("created") diff --git a/ietfSig/shared/src/main/scala/run/cosy/http/messages/AtIds.scala b/ietfSig/shared/src/main/scala/run/cosy/http/messages/AtIds.scala index 9591c1d..1fd0544 100644 --- a/ietfSig/shared/src/main/scala/run/cosy/http/messages/AtIds.scala +++ b/ietfSig/shared/src/main/scala/run/cosy/http/messages/AtIds.scala @@ -18,15 +18,15 @@ package run.cosy.http.messages import run.cosy.http.messages.AtId.* object AtIds: - val `method` = AtId("@method").get - val `request-target` = AtId("@request-target").get - val `target-uri` = AtId("@target-uri").get - val `authority` = AtId("@authority").get - val `scheme` = AtId("@scheme").get - val `path` = AtId("@path").get - val `query` = AtId("@query").get - val `query-param` = AtId("@query-param").get - val `status` = AtId("@status").get + val `method` = AtId("@method").toTry.get + val `request-target` = AtId("@request-target").toTry.get + val `target-uri` = AtId("@target-uri").toTry.get + val `authority` = AtId("@authority").toTry.get + val `scheme` = AtId("@scheme").toTry.get + val `path` = AtId("@path").toTry.get + val `query` = AtId("@query").toTry.get + val `query-param` = AtId("@query-param").toTry.get + val `status` = AtId("@status").toTry.get val requestIds = List(method, `request-target`, `target-uri`, authority, scheme, path, query, `query-param`) diff --git a/ietfSig/shared/src/main/scala/run/cosy/http/messages/AtSelectors.scala b/ietfSig/shared/src/main/scala/run/cosy/http/messages/AtSelectors.scala index 1766be8..210db92 100644 --- a/ietfSig/shared/src/main/scala/run/cosy/http/messages/AtSelectors.scala +++ b/ietfSig/shared/src/main/scala/run/cosy/http/messages/AtSelectors.scala @@ -20,6 +20,7 @@ import run.cosy.http.Http import run.cosy.http.headers.Rfc8941 import run.cosy.http.headers.Rfc8941.{Params, SfString} import cats.data.NonEmptyList +import run.cosy.http.auth.ParsingExc import scala.collection.immutable.ListMap import scala.util.{Success, Try} @@ -30,16 +31,16 @@ open class AtRequestSel[F[_], H <: Http]( val selectorFn: SelectorFn[Http.Request[F, H]], override val params: Rfc8941.Params = ListMap() ) extends RequestSelector[F, H]: - override def renderNel(nel: NonEmptyList[String]): Try[String] = - Success(nel.map(identifier + _).toList.mkString("\n")) + override def renderNel(nel: NonEmptyList[String]): Either[ParsingExc, String] = + Right(nel.map(header + _).toList.mkString("\n")) open class AtResponseSel[F[_], H <: Http]( val name: AtId, val selectorFn: SelectorFn[Http.Response[F, H]], override val params: Rfc8941.Params = ListMap() ) extends ResponseSelector[F, H]: - override def renderNel(nel: NonEmptyList[String]): Try[String] = - Success(nel.map(identifier + _).toList.mkString("\n")) + override def renderNel(nel: NonEmptyList[String]): Either[ParsingExc, String] = + Right(nel.map(header + _).toList.mkString("\n")) trait AtSelectors[F[_], H <: Http](using sf: AtSelectorFns[F, H]): diff --git a/ietfSig/shared/src/main/scala/run/cosy/http/messages/ComponentId.scala b/ietfSig/shared/src/main/scala/run/cosy/http/messages/ComponentId.scala index 3982f5f..b7eaeea 100644 --- a/ietfSig/shared/src/main/scala/run/cosy/http/messages/ComponentId.scala +++ b/ietfSig/shared/src/main/scala/run/cosy/http/messages/ComponentId.scala @@ -16,7 +16,7 @@ package run.cosy.http.messages -import run.cosy.http.auth.SelectorException +import run.cosy.http.auth.{ParsingExc, SelectorException} import run.cosy.http.headers.Rfc8941 import run.cosy.http.headers.Rfc8941.SfString import run.cosy.http.messages.Selectors.{CollationTp, SelFormat} @@ -57,16 +57,20 @@ object AtId: * something like https://softwaremill.com/fancy-strings-in-scala-3/ (otherwise: that is what * unit tests are for) */ - def apply(name: String): Try[AtId] = Try { - if name.length == 0 then throw SelectorException("Message Component must start with @ char ") - else if name.head != '@' then - throw SelectorException("Message Component must start with @ char ") - else - ComponentId.parse(name.tail) match - case err: String => throw SelectorException(err) - case tk: Rfc8941.Token => new AtId: - override val lcname: Rfc8941.Token = tk - } + def apply(name: String): Either[SelectorException, AtId] = + try + if name.length == 0 then + throw SelectorException("Message Component must start with @ char ") + else if name.head != '@' then + throw SelectorException("Message Component must start with @ char ") + else + ComponentId.parse(name.tail) match + case err: String => throw SelectorException(err) + case tk: Rfc8941.Token => Right( + new AtId: + override val lcname: Rfc8941.Token = tk + ) + catch case e: SelectorException => Left(e) end AtId diff --git a/ietfSig/shared/src/main/scala/run/cosy/http/messages/HeaderSelectors.scala b/ietfSig/shared/src/main/scala/run/cosy/http/messages/HeaderSelectors.scala index 1cd6751..0d2479b 100644 --- a/ietfSig/shared/src/main/scala/run/cosy/http/messages/HeaderSelectors.scala +++ b/ietfSig/shared/src/main/scala/run/cosy/http/messages/HeaderSelectors.scala @@ -21,6 +21,7 @@ import run.cosy.http.headers.Rfc8941 import cats.data.NonEmptyList import scala.util.{Try, Success, Failure} import scala.collection.immutable.ListMap +import run.cosy.http.auth.ParsingExc class RequestHeaderSel[F[_], H <: Http]( override val name: HeaderId, @@ -28,8 +29,8 @@ class RequestHeaderSel[F[_], H <: Http]( override val selectorFn: SelectorFn[Http.Request[F, H]], override val params: Rfc8941.Params = ListMap() ) extends RequestSelector[F, H]: - override def renderNel(nel: NonEmptyList[String]): Try[String] = - Selectors.render(name, collTp)(nel).map(identifier + _) + override def renderNel(nel: NonEmptyList[String]): Either[ParsingExc, String] = + Selectors.render(name, collTp)(nel).map(header + _) class ResponseHeaderSel[F[_], H <: Http]( override val name: HeaderId, @@ -37,8 +38,8 @@ class ResponseHeaderSel[F[_], H <: Http]( override val selectorFn: SelectorFn[Http.Response[F, H]], override val params: Rfc8941.Params = ListMap() ) extends ResponseSelector[F, H]: - override def renderNel(nel: NonEmptyList[String]): Try[String] = - Selectors.render(name, collTp)(nel).map(identifier + _) + override def renderNel(nel: NonEmptyList[String]): Either[ParsingExc, String] = + Selectors.render(name, collTp)(nel).map(header + _) trait HeaderSelectors[F[_], H <: Http](using sf: HeaderSelectorFns[F, H]): import Selectors.CollationTp @@ -47,7 +48,7 @@ trait HeaderSelectors[F[_], H <: Http](using sf: HeaderSelectorFns[F, H]): * @collTp * the way to interpret the headers values returned */ - def requestHeader(name: HeaderId)( + def onRequest(name: HeaderId)( collTp: name.AllowedCollation ): RequestSelector[F, H] = new RequestHeaderSel[F, H]( @@ -57,7 +58,7 @@ trait HeaderSelectors[F[_], H <: Http](using sf: HeaderSelectorFns[F, H]): collTp.toParam ) - def responseHeader(name: HeaderId)( + def onResponse(name: HeaderId)( collTp: name.AllowedCollation ): ResponseSelector[F, H] = new ResponseHeaderSel[F, H]( @@ -78,52 +79,52 @@ trait HeaderSelectors[F[_], H <: Http](using sf: HeaderSelectorFns[F, H]): import Selectors.{SelFormat, CollationTp as Ct} import run.cosy.http.messages.HeaderIds.Request as req - lazy val accept = requestHeader(retro.accept) + lazy val accept = onRequest(retro.accept) /* could not find authorization in retrofit. Could one send more than one? Yet, I think. */ - lazy val authorization = requestHeader(req.authorization) - lazy val `cache-control` = requestHeader(retro.`cache-control`) - lazy val `content-type` = requestHeader(retro.`content-type`) + lazy val authorization = onRequest(req.authorization) + lazy val `cache-control` = onRequest(retro.`cache-control`) + lazy val `content-type` = onRequest(retro.`content-type`) /** a list because sometimes multiple lengths are sent! */ - lazy val `content-length` = requestHeader(retro.`content-length`) - lazy val signature_sf = requestHeader(req.signature) + lazy val `content-length` = onRequest(retro.`content-length`) + lazy val signature_sf = onRequest(req.signature) // todo: it feels like dict objs should allow the user to select the type cloer to the point // of creating the signature. - lazy val signature_dict = requestHeader(req.signature) - lazy val `content-digest` = requestHeader(req.`content-digest`) + lazy val signature_dict = onRequest(req.signature) + lazy val `content-digest` = onRequest(req.`content-digest`) /** Defined in: * [[https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-client-cert-field-03#section-2 httpbis-client-cert]] */ - lazy val `client-cert` = requestHeader(req.`client-cert`) - lazy val `client-cert-chain` = requestHeader(req.`client-cert-chain`) - lazy val forwarded = requestHeader(req.forwarded) + lazy val `client-cert` = onRequest(req.`client-cert`) + lazy val `client-cert-chain` = onRequest(req.`client-cert-chain`) + lazy val forwarded = onRequest(req.forwarded) end RequestHd object ResponseHd: import Selectors.{SelFormat, CollationTp as Ct} import run.cosy.http.messages.HeaderIds.Response as res - lazy val `accept-post` = responseHeader(retro.`accept-post`) - lazy val `cache-control` = responseHeader(res.`cache-control`) + lazy val `accept-post` = onResponse(retro.`accept-post`) + lazy val `cache-control` = onResponse(res.`cache-control`) /** Clients cannot send date headers via JS. Also not really useful as the time is available * in the Signature-Input in unix time. see * [[https://www.rfc-editor.org/rfc/rfc9110.html#section-8.6 rfc9110]] for handling * requirements (from httpbis-retrofit) */ - lazy val date = responseHeader(res.date) - lazy val `content-type` = responseHeader(res.`content-type`) - lazy val `content-length` = responseHeader(res.`content-length`) + lazy val date = onResponse(res.date) + lazy val `content-type` = onResponse(res.`content-type`) + lazy val `content-length` = onResponse(res.`content-length`) /** One could create a new parameter to convert to SF-ETAG specified by httpbis-retrofit */ - lazy val etag = responseHeader(res.etag) - lazy val signature = responseHeader(res.signature) + lazy val etag = onResponse(res.etag) + lazy val signature = onResponse(res.signature) /** [[https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-digest-headers-10#section-2 * digest-headers draft rfc] */ lazy val `content-digest` = - responseHeader(res.`content-digest`) + onResponse(res.`content-digest`) end ResponseHd end HeaderSelectors diff --git a/ietfSig/shared/src/main/scala/run/cosy/http/messages/RequestSelectorDB.scala b/ietfSig/shared/src/main/scala/run/cosy/http/messages/RequestSelectorDB.scala index 95e5a43..dd7b974 100644 --- a/ietfSig/shared/src/main/scala/run/cosy/http/messages/RequestSelectorDB.scala +++ b/ietfSig/shared/src/main/scala/run/cosy/http/messages/RequestSelectorDB.scala @@ -18,7 +18,7 @@ package run.cosy.http.messages import cats.data.NonEmptyList import run.cosy.http.Http -import run.cosy.http.auth.{AttributeException, SelectorException} +import run.cosy.http.auth.{AttributeException, ParsingExc, SelectorException} import run.cosy.http.headers.Rfc8941 import run.cosy.http.headers.Rfc8941.Syntax.sf import run.cosy.http.messages.HeaderId.SfHeaderId @@ -36,7 +36,7 @@ import scala.util.{Failure, Success, Try} * @param headerTypeDB * Database mapping header ids to the recognised way of encoding that header */ -class RequestSelectorDB[F[_], H <: Http]( +case class RequestSelectorDB[F[_], H <: Http]( atSel: AtSelectors[F, H], knownIds: Seq[HeaderId], selFns: HeaderSelectorFns[F, H] @@ -63,19 +63,19 @@ class RequestSelectorDB[F[_], H <: Http]( (AtIds.requestIds.toSeq ++ knownIds).map(id => id.specName -> id).toMap // we get this info from the Signing-String header - def get(id: String, params: Rfc8941.Params): Try[RequestSelector[F, H]] = + def get(id: String, params: Rfc8941.Params): Either[ParsingExc, RequestSelector[F, H]] = componentIds.get(id) match case Some(at: AtId) => atComponentMap.get(at) match - case Some(sel) => Success(sel) + case Some(sel) => Right(sel) case None => if at == AtIds.`query-param` then params.get(nameTk).collect { case p: SfString => atSel.`@query-param`(p) }.toRight(SelectorException( s"Wrong parameter for @query-param. Received: >$params<" - )).toTry + )) else - Failure( + Left( SelectorException( s"component >$id< is not valid msg component name for requests" ) @@ -90,54 +90,58 @@ class RequestSelectorDB[F[_], H <: Http]( selFns.requestHeaders(hdrId), params // we pass the params as received since they have been filtered for sanity ) - case None => Failure(SelectorException(s"we don't recognised component >$id<")) + case None => Left(SelectorException(s"we don't recognised component >$id<")) /** return the collation type for the this header selector id as requested by the given * parameters. Check with headerTypeDB if the requests are valid for intepreted types this * ignores parameters it does not understand. May not be the right behavior. todo: check */ - def interpretParams(id: HeaderId, params: Rfc8941.Params): Try[id.AllowedCollation] = - Try { - // 1. we translate all the parameters to well typed CollationTps and forget anything else - val tps: Seq[CollationTp] = params.collect { - case (`keyTk`, str: Rfc8941.SfString) => id match - case sfId: SfHeaderId if sfId.format == SelFormat.Dictionary => Selectors.DictSel(str) - case _ => throw AttributeException( - s"we don't know that header >$id< can be interpreted as a dictionary" + def interpretParams( + id: HeaderId, + params: Rfc8941.Params + ): Either[AttributeException, id.AllowedCollation] = + try + // 1. we translate all the parameters to well typed CollationTps and forget anything else + val tps: Seq[CollationTp] = params.collect { + case (`keyTk`, str: Rfc8941.SfString) => id match + case sfId: SfHeaderId if sfId.format == SelFormat.Dictionary => + Selectors.DictSel(str) + case _ => throw AttributeException( + s"we don't know that header >$id< can be interpreted as a dictionary" + ) + case (`keyTk`, x) => + throw AttributeException(s"key value can only be of type SfString. Received >$x< ") + case (`sfTk`, true) => id match + case sfId: SfHeaderId => Selectors.Strict + case _ => throw AttributeException( + s"We don't know what the agreed type for parsing header >$id< as sf is." + ) + case (`sfTk`, x) => + throw AttributeException(s"value of attributed 'sf' can only be true. Received >$x<") + case (`bsTk`, true) => Selectors.Bin + case (`bsTk`, x) => + throw AttributeException(s"value of attributed 'bs' can only be true. Received >$x<") + }.toSeq + // 2. now we have to detect inconsistencies and reduce + val ct = + if tps.contains(Selectors.Bin) + then // a. is it binary? then check if it is inconsistent, or return + if tps.exists(ct => + ct.isInstanceOf[Selectors.DictSel] || ct == Selectors.Strict ) - case (`keyTk`, x) => - throw AttributeException(s"key value can only be of type SfString. Received >$x< ") - case (`sfTk`, true) => id match - case sfId: SfHeaderId => Selectors.Strict - case _ => throw AttributeException( - s"We don't know what the agreed type for parsing header >$id< as sf is." + then + throw AttributeException( + "We cannot have attributes 'sf' or 'key' together with 'sb' on a header component" ) - case (`sfTk`, x) => - throw AttributeException(s"value of attributed 'sf' can only be true. Received >$x<") - case (`bsTk`, true) => Selectors.Bin - case (`bsTk`, x) => - throw AttributeException(s"value of attributed 'bs' can only be true. Received >$x<") - }.toSeq - // 2. now we have to detect inconsistencies and reduce - val ct = - if tps.contains(Selectors.Bin) - then // a. is it binary? then check if it is inconsistent, or return - if tps.exists(ct => - ct.isInstanceOf[Selectors.DictSel] || ct == Selectors.Strict - ) - then - throw AttributeException( - "We cannot have attributes 'sf' or 'key' together with 'sb' on a header component" - ) - else Selectors.Bin - else // b. if not binary, then - tps.find(_.isInstanceOf[Selectors.DictSel]) orElse { - tps.find(_ == Selectors.Strict) - } getOrElse { - Selectors.Raw - } - // todo: find a way of not using xx_instnaceOf methods - ct.asInstanceOf[id.AllowedCollation] - } + else Selectors.Bin + else // b. if not binary, then + tps.find(_.isInstanceOf[Selectors.DictSel]) orElse { + tps.find(_ == Selectors.Strict) + } getOrElse { + Selectors.Raw + } + // todo: find a way of not using xx_instnaceOf methods + Right(ct.asInstanceOf[id.AllowedCollation]) + catch case e: AttributeException => Left(e) end RequestSelectorDB diff --git a/ietfSig/shared/src/main/scala/run/cosy/http/messages/Selector.scala b/ietfSig/shared/src/main/scala/run/cosy/http/messages/Selector.scala index 933cc71..8fe886c 100644 --- a/ietfSig/shared/src/main/scala/run/cosy/http/messages/Selector.scala +++ b/ietfSig/shared/src/main/scala/run/cosy/http/messages/Selector.scala @@ -23,7 +23,12 @@ import scala.util.Try import scala.collection.immutable.{ArraySeq, ListMap} import scala.util.{Failure, Success, Try} import run.cosy.http.headers.Rfc8941.Serialise.given -import run.cosy.http.auth.{AttributeException, HTTPHeaderParseException, SelectorException} +import run.cosy.http.auth.{ + AttributeException, + HTTPHeaderParseException, + ParsingExc, + SelectorException +} import run.cosy.http.messages.Parameters.{bsTk, keyTk, nameTk, reqTk, sfTk} import run.cosy.http.headers.{ParsingException, Rfc8941} import run.cosy.http.headers.Rfc8941.SfDict @@ -44,7 +49,7 @@ trait SelectorFn[Msg]: * of ways. * The result is a String for simple methods such as @query * */ - val signingValues: Msg => Try[String | NonEmptyList[String]] + val signingValues: Msg => Either[ParsingExc, String | NonEmptyList[String]] end SelectorFn /** Selector for components starting with @ take whole messages as parameters In the spec they are @@ -80,14 +85,15 @@ trait Selector: val selectorFn: SelectorFn[Msg] /** this is the implementation for @query-param, only. Override on headers. */ - def renderNel(nel: NonEmptyList[String]): Try[String] + def renderNel(nel: NonEmptyList[String]): Either[ParsingExc, String] - def signingStr(msg: Msg): Try[String] = + def signingStr(msg: Msg): Either[ParsingExc, String] = selectorFn.signingValues(msg).flatMap { case nel: NonEmptyList[String] => renderNel(nel) - case str => Success(identifier + str) + case str => Right(header + str) } + def header: String = identifier + ": " def identifier: String = val attrs = if params.isEmpty then "" @@ -96,7 +102,7 @@ trait Selector: if value.isInstanceOf[Boolean] then key.canon else key.canon + "=" + value.canon ).mkString(";", ";", "") - s"""${name.canon}$attrs: """ + s"""${name.canon}$attrs""" end identifier end Selector diff --git a/ietfSig/shared/src/main/scala/run/cosy/http/messages/SelectorOps.scala b/ietfSig/shared/src/main/scala/run/cosy/http/messages/SelectorOps.scala index c122dc1..82f352d 100644 --- a/ietfSig/shared/src/main/scala/run/cosy/http/messages/SelectorOps.scala +++ b/ietfSig/shared/src/main/scala/run/cosy/http/messages/SelectorOps.scala @@ -17,6 +17,7 @@ package run.cosy.http.messages import run.cosy.http.headers.Rfc8941 +import run.cosy.http.headers.Rfc8941.Serialise.given import run.cosy.http.headers.SigInput import scala.util.{Try, Success, Failure} import cats.data.NonEmptyList @@ -28,3 +29,4 @@ object `@signature-params`: val lowercaseName = "@signature-params" val pitem = Rfc8941.PItem(Rfc8941.SfString(lowercaseName)) def signingStr(sigInput: SigInput): String = s""""@signature-params": ${sigInput.canon}""" + def paramStr(sigInput: SigInput): String = sigInput.il.params.canon diff --git a/ietfSig/shared/src/main/scala/run/cosy/http/messages/Selectors.scala b/ietfSig/shared/src/main/scala/run/cosy/http/messages/Selectors.scala index 13b78c4..4f22477 100644 --- a/ietfSig/shared/src/main/scala/run/cosy/http/messages/Selectors.scala +++ b/ietfSig/shared/src/main/scala/run/cosy/http/messages/Selectors.scala @@ -17,20 +17,21 @@ package run.cosy.http.messages import cats.data.NonEmptyList +import cats.syntax.all.* import run.cosy.http.Http import run.cosy.http.Http.Request -import run.cosy.http.auth.{HTTPHeaderParseException, SelectorException} -import run.cosy.http.headers.Rfc8941 +import run.cosy.http.auth.{HTTPHeaderParseException, ParsingExc, SelectorException} import run.cosy.http.headers.Rfc8941.Serialise.given import run.cosy.http.headers.Rfc8941.Syntax.sf import run.cosy.http.headers.Rfc8941.{Params, SfDict, SfList, SfString} +import run.cosy.http.headers.{ParsingException, Rfc8941} import run.cosy.http.messages.HeaderId.SfHeaderId import run.cosy.http.messages.Selectors.{CollationTp, SelFormat} import run.cosy.http.messages.{AtSelectorFns, Parameters} +import scodec.bits.ByteVector import scala.collection.immutable.{ArraySeq, HashMap, ListMap} import scala.util.{Failure, Success, Try} -import scodec.bits.ByteVector class Selectors[F[_], H <: Http](using SelectorFns[F, H]) extends AtSelectors[F, H] with HeaderSelectors[F, H] @@ -38,73 +39,10 @@ class Selectors[F[_], H <: Http](using SelectorFns[F, H]) object Selectors: import Parameters.* - /** When a header is named sf this means it can be interpreted as a structured header. This map - * specifies how to interpret them. for information on how existing headers can be understood as - * working with rfc8941 see - * [[https://greenbytes.de/tech/webdav/draft-ietf-httpbis-retrofit-latest.html Retrofit Structured Fields for HTTP]] - * olso some good overview documentation on - * [[https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers HTTP Headers]] - */ - val knownHeaders: Map[String, Selectors.SelFormat] = HashMap( - "accept" -> SelFormat.List, - "accept-encoding" -> SelFormat.List, - "accept-language" -> SelFormat.List, - "accept-patch" -> SelFormat.List, - "accept-post" -> SelFormat.List, - "accept-ranges" -> SelFormat.List, - "access-control-allow-credentials" -> SelFormat.Item, - "access-control-allow-headers" -> SelFormat.List, - "access-control-allow-methods" -> SelFormat.List, - "access-control-allow-origin" -> SelFormat.Item, - "access-control-expose-headers" -> SelFormat.List, - "access-control-max-age" -> SelFormat.Item, - "access-control-request-headers" -> SelFormat.List, - "access-control-request-method" -> SelFormat.Item, - "age" -> SelFormat.Item, - "allow" -> SelFormat.List, - "alpn" -> SelFormat.List, - "alt-svc" -> SelFormat.Dictionary, - "alt-used" -> SelFormat.Item, - "cache-control" -> SelFormat.Dictionary, - "cdn-loop" -> SelFormat.List, - "clear-site-data" -> SelFormat.List, - "connection" -> SelFormat.List, - "content-encoding" -> SelFormat.List, - "content-language" -> SelFormat.List, - "content-length" -> SelFormat.List, - "content-type" -> SelFormat.Item, - "cross-origin-resource-policy" -> SelFormat.Item, - "dnt" -> SelFormat.Item, - "expect" -> SelFormat.Dictionary, - "expect-ct" -> SelFormat.Dictionary, - "host" -> SelFormat.Item, - "keep-alive" -> SelFormat.Dictionary, - "max-forwards" -> SelFormat.Item, - "origin" -> SelFormat.Item, - "pragma" -> SelFormat.Dictionary, - "prefer" -> SelFormat.Dictionary, - "preference-applied" -> SelFormat.Dictionary, - "retry-after" -> SelFormat.Item, - "sec-websocket-extensions" -> SelFormat.List, - "sec-websocket-protocol" -> SelFormat.List, - "sec-websocket-version" -> SelFormat.Item, - "server-timing" -> SelFormat.List, - "surrogate-control" -> SelFormat.Dictionary, - "te" -> SelFormat.List, - "timing-allow-origin" -> SelFormat.List, - "trailer" -> SelFormat.List, - "transfer-encoding" -> SelFormat.List, - "upgrade-insecure-requests" -> SelFormat.Item, - "vary" -> SelFormat.List, - "x-content-type-options" -> SelFormat.Item, - "x-frame-options" -> SelFormat.Item, - "x-xss-protection" -> SelFormat.List - ) - def render( id: HeaderId, tp: id.AllowedCollation - )(headerValues: NonEmptyList[String]): Try[String] = + )(headerValues: NonEmptyList[String]): Either[ParsingExc, String] = tp match case Raw => raw(headerValues) case Strict => sfValue(headerValues, id.asInstanceOf[SfHeaderId].format) @@ -112,7 +50,7 @@ object Selectors: case DictSel(key) => sfDictNameSelector(key, headerValues) end render - def sfValue(headers: NonEmptyList[String], interp: SelFormat): Try[String] = + def sfValue(headers: NonEmptyList[String], interp: SelFormat): Either[ParsingExc, String] = val combinedHeaders = headers.toList.mkString(", ") // sfParse(combinedHeaders).map{ interp match @@ -122,37 +60,39 @@ object Selectors: def sfParseItem( headerValue: String - ): Try[Rfc8941.PItem[?]] = - Rfc8941.Parser.sfItem.parseAll(headerValue) match - case Left(err) => Failure(HTTPHeaderParseException(err, headerValue)) - case Right(dict) => Success(dict) + ): Either[HTTPHeaderParseException, Rfc8941.PItem[?]] = + Rfc8941.Parser.sfItem.parseAll(headerValue).leftMap(err => + HTTPHeaderParseException(err, headerValue) + ) def sfParseDict( headerValue: String - ): Try[Rfc8941.SfDict] = - Rfc8941.Parser.sfDictionary.parseAll(headerValue) match - case Left(err) => Failure(HTTPHeaderParseException(err, headerValue)) - case Right(dict) => Success(dict) + ): Either[HTTPHeaderParseException, Rfc8941.SfDict] = + Rfc8941.Parser.sfDictionary.parseAll(headerValue).leftMap(err => + HTTPHeaderParseException(err, headerValue) + ) /** Dict selector with name param */ def sfDictNameSelector( nameSelector: Rfc8941.SfString, headers: NonEmptyList[String] - ): Try[String] = + ): Either[ParsingExc, String] = val combinedHeaders = headers.toList.mkString(", ") for dict <- sfParseDict(combinedHeaders) - name <- Try(Rfc8941.Token(nameSelector.asciiStr)) + name <- Rfc8941.Parser.sfToken.parseAll(nameSelector.asciiStr).leftMap(p => + HTTPHeaderParseException(p, s"could not convert >$nameSelector< to token") + ) value <- dict.get(name) .toRight(SelectorException( s"No dictionary element >$nameSelector< in with value >$combinedHeaders" - )).toTry + )) yield value.canon - def raw(headers: NonEmptyList[String]): Try[String] = - Success(headers.map(_.trim).toList.mkString(", ")) + def raw(headers: NonEmptyList[String]): Either[ParsingExc, String] = + Right(headers.map(_.trim).toList.mkString(", ")) - def bin(headers: NonEmptyList[String]): Try[String] = Try { + def bin(headers: NonEmptyList[String]): Either[ParsingExc, String] = Right { headers.map(h => ByteVector.view(h.trim.nn.getBytes(java.nio.charset.StandardCharsets.US_ASCII).nn).canon ).toList.mkString(", ") @@ -160,10 +100,10 @@ object Selectors: def sfParseList( headerValue: String - ): Try[Rfc8941.SfList] = - Rfc8941.Parser.sfList.parseAll(headerValue) match - case Left(err) => Failure(HTTPHeaderParseException(err, headerValue)) - case Right(dict) => Success(dict) + ): Either[HTTPHeaderParseException, Rfc8941.SfList] = + Rfc8941.Parser.sfList.parseAll(headerValue).leftMap(err => + HTTPHeaderParseException(err, headerValue) + ) sealed trait CollationTp: def toParam: Params = this match diff --git a/ietfSigTests/shared/src/main/scala/run/cosy/http/dummy/DummyHttp.scala b/ietfSigTests/shared/src/main/scala/run/cosy/http/dummy/DummyHttp.scala index 68ee858..82be03b 100644 --- a/ietfSigTests/shared/src/main/scala/run/cosy/http/dummy/DummyHttp.scala +++ b/ietfSigTests/shared/src/main/scala/run/cosy/http/dummy/DummyHttp.scala @@ -10,7 +10,7 @@ import scala.util.Try import cats.data.NonEmptyList import scala.util.{Failure, Success} -import run.cosy.http.auth.SelectorException +import run.cosy.http.auth.{ParsingExc, SelectorException} import _root_.org.typelevel.ci.CIString object DummyHttp extends Http: @@ -63,22 +63,22 @@ object DummyHeaderSelectorFns extends messages.HeaderSelectorFns[Id,DHT]: override def requestHeaders(name: HeaderId): RequestFn = new messages.SelectorFn[Http.Request[Id,DHT]]: - override val signingValues: Request[Id, DHT] => Try[String|NonEmptyList[String]] = + override val signingValues: Request[Id, DHT] => Either[ParsingExc, String|NonEmptyList[String]] = req => msgSel(name, req.asInstanceOf[Seq[(String,String)]]) end requestHeaders - def msgSel(name: HeaderId, msg: Seq[(String, String)]): Try[String|NonEmptyList[String]] = + def msgSel(name: HeaderId, msg: Seq[(String, String)]): Either[ParsingExc,String|NonEmptyList[String]] = msg.groupBy(_._1).get(CIString(name.specName)) match - case None => Failure(SelectorException("no header named: "+name)) + case None => Left(SelectorException("no header named: "+name)) case Some(pairs) => val v: Seq[String] = pairs.map(_._2) - Success(NonEmptyList(v.head,v.tail.toList)) + Right(NonEmptyList(v.head,v.tail.toList)) end msgSel override def responseHeaders(name: HeaderId): ResponseFn = new messages.SelectorFn[Http.Response[Id,DHT]]: - override val signingValues: Response[Id, DHT] => Try[String | NonEmptyList[String]] = + override val signingValues: Response[Id, DHT] => Either[ParsingExc, String | NonEmptyList[String]] = res => msgSel(name, res.asInstanceOf[Seq[(String,String)]]) end DummyHeaderSelectorFns diff --git a/ietfSigTests/shared/src/main/scala/run/cosy/http/messages/AtSelectorSuite.scala b/ietfSigTests/shared/src/main/scala/run/cosy/http/messages/AtSelectorSuite.scala index ea18c65..1c0c2e3 100644 --- a/ietfSigTests/shared/src/main/scala/run/cosy/http/messages/AtSelectorSuite.scala +++ b/ietfSigTests/shared/src/main/scala/run/cosy/http/messages/AtSelectorSuite.scala @@ -2,6 +2,7 @@ package run.cosy.http.messages import munit.CatsEffectSuite import run.cosy.http.Http.{Request, Response} +import run.cosy.http.auth.ParsingExc import run.cosy.http.headers.Rfc8941.Syntax.sf import run.cosy.http.messages.{AtSelectors, ServerContext} import run.cosy.http.{Http, auth} @@ -17,14 +18,14 @@ trait AtSelectorSuite[F[_], H <: Http] extends CatsEffectSuite: val sc: ServerContext = ServerContext("www.example.com", true) val req: Http.Request[F, H] = interp.asRequest(HttpMessageDB.`2.2.1_Method_POST`) val sigStr = sel(sc).`@method`.signingStr(req) - assertEquals(sigStr, Success(""""@method": POST""")) + assertEquals(sigStr, Right(""""@method": POST""")) } def reqFail(selector: RequestSelector[F, H], req: Request[F, H]): Unit = - val result: Try[String] = selector.signingStr(req) + val result: Try[String] = selector.signingStr(req).toTry assert(result.isFailure, result) - def resS(meth: String, res: String, attrs: (String, String)*): Try[String] = Try { + def resS(meth: String, res: String, attrs: (String, String)*): Either[ParsingExc,String] = Right { val ats = for (k, v) <- attrs.toSeq yield s"""$k="$v"""" val optAtts = if ats.isEmpty then "" else ats.mkString(";", ";", "") s""""$meth"$optAtts: $res""" @@ -95,7 +96,7 @@ trait AtSelectorSuite[F[_], H <: Http] extends CatsEffectSuite: assertEquals(`@query`.signingStr(req), resS("@query", "?param=value¶m=another")) assertEquals( `@query-param`(sf"param").signingStr(req), - Success(""""@query-param";name="param": value + Right(""""@query-param";name="param": value |"@query-param";name="param": another""".stripMargin) ) @@ -204,7 +205,7 @@ trait AtSelectorSuite[F[_], H <: Http] extends CatsEffectSuite: assertEquals(`@query`.signingStr(req), resS("@query", "?queryString")) assertEquals( `@query-param`(sf"queryString").signingStr(req), - Success(""""@query-param";name="queryString": """) + Right(""""@query-param";name="queryString": """) ) reqFail(`@query-param`(sf"q"), req) // we use the server context @@ -235,7 +236,7 @@ trait AtSelectorSuite[F[_], H <: Http] extends CatsEffectSuite: assertEquals(`@query`.signingStr(req), resS("@query", "?param=value&foo=bar&baz=batman&qux=")) assertEquals( `@query-param`(sf"param").signingStr(req), - Success(""""@query-param";name="param": value""") + Right(""""@query-param";name="param": value""") ) // we use the server context assertEquals(`@authority`.signingStr(req), resS("@authority", "bblfish.net")) @@ -256,7 +257,7 @@ trait AtSelectorSuite[F[_], H <: Http] extends CatsEffectSuite: // because of typesafety we can only make one test here assertEquals(`@status`.signingStr(res), resS("@status", "200")) - assertEquals(`@status`.signingStr(res), Success(""""@status": 200""")) + assertEquals(`@status`.signingStr(res), Right(""""@status": 200""")) } end AtSelectorSuite diff --git a/ietfSigTests/shared/src/main/scala/run/cosy/http/messages/HeaderSuite.scala b/ietfSigTests/shared/src/main/scala/run/cosy/http/messages/HeaderSuite.scala index 2047c21..3e2e23d 100644 --- a/ietfSigTests/shared/src/main/scala/run/cosy/http/messages/HeaderSuite.scala +++ b/ietfSigTests/shared/src/main/scala/run/cosy/http/messages/HeaderSuite.scala @@ -22,11 +22,12 @@ import _root_.run.cosy.http.Http import _root_.run.cosy.http.Http.Request import _root_.run.cosy.http.headers.Rfc8941 import _root_.run.cosy.http.headers.Rfc8941.Token -import _root_.run.cosy.http.messages.{HeaderSelectors, TestHttpMsgInterpreter, RequestSelector, HttpMessageDB as DB, Selectors} +import _root_.run.cosy.http.messages.{HeaderSelectors, RequestSelector, Selectors, TestHttpMsgInterpreter, HttpMessageDB as DB} import _root_.run.cosy.platform import cats.data.NonEmptyList import cats.effect.Async import munit.CatsEffectSuite +import run.cosy.http.auth.ParsingExc import scodec.bits.ByteVector import java.util.Locale @@ -64,15 +65,15 @@ open class HeaderSuite[F[_], H <: Http]( import HeaderId.{OldId,DictId,ListId,ItemId} // special headers used in the spec that we won't find elsewhere - val `x-example` = selectr.requestHeader(exHd.`x-example`) - val `x-empty-header` = selectr.requestHeader(exHd.`x-empty-header`) - val `x-ows-header` = selectr.requestHeader(exHd.`x-ows-header`) - val `x-obs-fold-header` = selectr.requestHeader(exHd.`x-obs-fold-header`) - val `example-dict` = selectr.requestHeader(exHd.`example-dict`) - val `example-header` = selectr.requestHeader(exHd.`example-header`) - val `cache-control` = selectr.requestHeader(hds.retrofit.`cache-control`) - val date = selectr.requestHeader(hds.Response.`date`) - val host = selectr.requestHeader(hds.retrofit.`host`) + val `x-example` = selectr.onRequest(exHd.`x-example`) + val `x-empty-header` = selectr.onRequest(exHd.`x-empty-header`) + val `x-ows-header` = selectr.onRequest(exHd.`x-ows-header`) + val `x-obs-fold-header` = selectr.onRequest(exHd.`x-obs-fold-header`) + val `example-dict` = selectr.onRequest(exHd.`example-dict`) + val `example-header` = selectr.onRequest(exHd.`example-header`) + val `cache-control` = selectr.onRequest(hds.retrofit.`cache-control`) + val date = selectr.onRequest(hds.Response.`date`) + val host = selectr.onRequest(hds.retrofit.`host`) import exHd.VerticalTAB val sfTk = Rfc8941.Token("sf") @@ -81,18 +82,15 @@ open class HeaderSuite[F[_], H <: Http]( val keyTk = Rfc8941.Token("key") val bsTk = Rfc8941.Token("bs") -// given ec: ExecutionContext = scala.concurrent.ExecutionContext.global -// given clock: Clock = -// Clock.fixed(java.time.Instant.ofEpochSecond(16188845000L), java.time.ZoneOffset.UTC).nn def expectedParamHeader(name: String, params: String, value: String) = - Success(s""""$name"$params: $value""") + Right(s""""$name"$params: $value""") - def expectedNameHeader(name: String, nameVal: String, value: String): Success[String] = - Success("\"" + name + "\";name=\"" + nameVal + "\": " + value) + def expectedNameHeader(name: String, nameVal: String, value: String): Right[ParsingExc, String] = + Right("\"" + name + "\";name=\"" + nameVal + "\": " + value) def expectedHeader(name: String, value: String) = - Success(s""""$name": $value""") + Right(s""""$name": $value""") // helper method extension (bytes: Try[ByteVector]) @@ -161,7 +159,7 @@ open class HeaderSuite[F[_], H <: Http]( test("§2.1.1 Strict Serialization of Dictionary Structured Header Request") { assertEquals( `example-dict`(Selectors.Strict).signingStr(`§2.1_HF`), - Success(""""example-dict";sf: a=1, b=2;x=1;y=2, c=(a b c), d""") + Right(""""example-dict";sf: a=1, b=2;x=1;y=2, c=(a b c), d""") ) } @@ -194,13 +192,13 @@ open class HeaderSuite[F[_], H <: Http]( `example-dict`(Selectors.DictSel(sf"q")).signingStr(`§2.1_HF`) ) failureTest( - selectr.requestHeader(HeaderId("dodo").get)(Selectors.Raw).signingStr(`§2.1_HF`) + selectr.onRequest(HeaderId("dodo").get)(Selectors.Raw).signingStr(`§2.1_HF`) ) failureTest( - selectr.requestHeader(HeaderId.dict("dodo").get)(Selectors.DictSel(sf"a")).signingStr(`§2.1_HF`) + selectr.onRequest(HeaderId.dict("dodo").get)(Selectors.DictSel(sf"a")).signingStr(`§2.1_HF`) ) failureTest( - selectr.requestHeader(HeaderId.dict("dodo").get)(Selectors.DictSel(sf"domino")).signingStr(`§2.1_HF`) + selectr.onRequest(HeaderId.dict("dodo").get)(Selectors.DictSel(sf"domino")).signingStr(`§2.1_HF`) ) } @@ -219,7 +217,7 @@ open class HeaderSuite[F[_], H <: Http]( ) } - def failureTest[X](shouldFail: Try[X]): Unit = - assert(shouldFail.isFailure, shouldFail) + def failureTest[X](shouldFail: Either[ParsingExc, X]): Unit = + assert(shouldFail.isLeft, shouldFail) end HeaderSuite diff --git a/ietfSigTests/shared/src/main/scala/run/cosy/http/messages/RequestSigSuite.scala b/ietfSigTests/shared/src/main/scala/run/cosy/http/messages/SigInputReqSuite.scala similarity index 93% rename from ietfSigTests/shared/src/main/scala/run/cosy/http/messages/RequestSigSuite.scala rename to ietfSigTests/shared/src/main/scala/run/cosy/http/messages/SigInputReqSuite.scala index f8fc908..feaf2ea 100644 --- a/ietfSigTests/shared/src/main/scala/run/cosy/http/messages/RequestSigSuite.scala +++ b/ietfSigTests/shared/src/main/scala/run/cosy/http/messages/SigInputReqSuite.scala @@ -3,7 +3,7 @@ package run.cosy.http.messages import bobcats.Verifier.SigningString import munit.CatsEffectSuite import run.cosy.http.Http.Request -import run.cosy.http.auth.MessageSignature +import run.cosy.http.auth.{MessageSignature, ParsingExc} import run.cosy.http.headers.Rfc8941.Serialise.given import run.cosy.http.headers.{Rfc8941, SigInput} import run.cosy.http.utils.StringUtils.* @@ -11,7 +11,7 @@ import run.cosy.http.{Http, HttpOps} import scala.util.{Failure, Success, Try} -open class RequestSigSuite[F[_], H <: Http]( +open class SigInputReqSuite[F[_], H <: Http]( msgSig: MessageSignature[F, H], rdb: RequestSelectorDB[F, H], msgDB: TestHttpMsgInterpreter[F, H] @@ -38,10 +38,10 @@ open class RequestSigSuite[F[_], H <: Http]( |;created=1618884473;keyid="test-key-rsa-pss"""".rfc8792single ) - val x: Try[SigningString] = req.signingStr(sigInput25.get) + val x: Either[ParsingExc, SigningString] = req.signatureBase(sigInput25.get) assertEquals( - x.flatMap(s => s.decodeAscii.toTry), - Success( + x.flatMap(s => s.decodeAscii), + Right( """"@method": POST |"@authority": example.com |"@path": /foo @@ -144,10 +144,10 @@ open class RequestSigSuite[F[_], H <: Http]( test(s"test req.signingStr $i: ${testSig.doc}") { val req = msgDB.asRequest(testSig.reqStr) val sigIn: Option[SigInput] = SigInput(testSig.sigInputStr) - val x: Try[SigningString] = req.signingStr(sigIn.get) + val x: Either[ParsingExc, SigningString] = req.signatureBase(sigIn.get) assertEquals( - x.flatMap(s => s.decodeAscii.toTry), - Success( + x.flatMap(s => s.decodeAscii), + Right( ((if testSig.baseResult == "" then List() else List(testSig.baseResult)) ::: List( """"@signature-params": """ + sigIn.get.canon )).mkString("\n") @@ -158,6 +158,7 @@ open class RequestSigSuite[F[_], H <: Http]( } test("static SigInput") { -// val at: rdb. + val `@` = rdb.atSel + val x = rdb.selFns } \ No newline at end of file diff --git a/ietfSigTests/shared/src/main/scala/run/cosy/http/messages/StaticSigInputReqSuite.scala b/ietfSigTests/shared/src/main/scala/run/cosy/http/messages/StaticSigInputReqSuite.scala new file mode 100644 index 0000000..4d5b5e7 --- /dev/null +++ b/ietfSigTests/shared/src/main/scala/run/cosy/http/messages/StaticSigInputReqSuite.scala @@ -0,0 +1,31 @@ +package run.cosy.http.messages + +import munit.CatsEffectSuite +import run.cosy.http.Http +import run.cosy.http.auth.MessageSignature +import run.cosy.http.messages.Selectors.* +import run.cosy.http.messages.TestHttpMsgInterpreter + +/** This test suite looks at statically built SigInput + * requests using functions. This is useful for + * clients writing signatures. + * */ +class StaticSigInputReqSuite[F[_], H <: Http]( + msgSig: MessageSignature[F, H], + hsel: HeaderSelectors[F, H], + atSel: AtSelectors[F, H], + interpret: TestHttpMsgInterpreter[F, H] +) extends CatsEffectSuite: + val hds = HeaderIds + + + import atSel.* + import hsel.RequestHd.* + val DB = HttpMessageDB + + test("develop api for using static SigInput selector") { + val req = interpret.asRequest(DB.`2.4_Req_Ex`) + val rsel: List[RequestSelector[F, H]] = + List(`@method`, `@authority`, `@path`, `content-digest`(Raw), `content-length`(Raw), `content-type`(Raw)) + + } \ No newline at end of file diff --git a/ietfSigTests/shared/src/main/scala/run/cosy/http/messages/TestHttpMsgInterpreter.scala b/ietfSigTests/shared/src/main/scala/run/cosy/http/messages/TestHttpMsgInterpreter.scala index 44ffe91..e20eb7b 100644 --- a/ietfSigTests/shared/src/main/scala/run/cosy/http/messages/TestHttpMsgInterpreter.scala +++ b/ietfSigTests/shared/src/main/scala/run/cosy/http/messages/TestHttpMsgInterpreter.scala @@ -15,8 +15,10 @@ trait TestHttpMsgInterpreter[F[_], H <: Http]: import run.cosy.http.messages.HttpMessageDB.{RequestStr,ResponseStr} + @throws[Throwable]("if the request string could not be interpreted") def asRequest(header: RequestStr): Http.Request[F, H] + @throws[Throwable]("if the response string could not be interpreted") def asResponse(header: ResponseStr): Http.Response[F,H] diff --git a/rfc8941/shared/src/main/scala/run/cosy/http/headers/Rfc8941.scala b/rfc8941/shared/src/main/scala/run/cosy/http/headers/Rfc8941.scala index 2406b68..5e010b6 100644 --- a/rfc8941/shared/src/main/scala/run/cosy/http/headers/Rfc8941.scala +++ b/rfc8941/shared/src/main/scala/run/cosy/http/headers/Rfc8941.scala @@ -27,6 +27,7 @@ import scala.reflect.TypeTest import scala.util.{Failure, Success, Try} import run.cosy.http.headers.NumberOutOfBoundsException import scodec.bits.ByteVector +import cats.syntax.all.* /** Structured Field Values for HTTP [[https://www.rfc-editor.org/rfc/rfc8941.html RFC8941]] */ @@ -42,7 +43,8 @@ object Rfc8941: type Params = ListMap[Token, Item] type SfList = List[Parameterized] type SfDict = ListMap[Token, Parameterized] - def Param(tk: String, i: Item): Param = (Token(tk), i) + // warning this is public and there is an unsafeParse + def Param(tk: String, i: Item): Param = (Token.unsafeParsed(tk), i) def Params(ps: Param*): Params = ListMap(ps*) def SfDict(entries: (Token, Parameterized)*): ListMap[Token, Parameterized] = ListMap(entries*) @@ -240,7 +242,7 @@ object Rfc8941: val lcalpha: P[Char] = P.charIn(0x61.toChar to 0x7a.toChar) | P.charIn('a' to 'z') val key: P[Token] = ((lcalpha | `*`) ~ (lcalpha | R5234.digit | P.charIn('_', '-', '.', '*')).rep0) - .map((c, lc) => Token((c :: lc).mkString)) + .map((c, lc) => Token.unsafeParsed((c :: lc).mkString)) val parameter: P[Param] = (key ~ (P.char('=') *> bareItem).orElse(P.pure(true))) // note: parameters always returns an answer (the empty list) as everything can have parameters