Skip to content

Commit

Permalink
Use security vocabulary for keyId doc as per solid/authentication-pan…
Browse files Browse the repository at this point in the history
  • Loading branch information
bblfish committed Apr 1, 2021
1 parent f3ed53e commit eca25e0
Show file tree
Hide file tree
Showing 16 changed files with 610 additions and 105 deletions.
29 changes: 22 additions & 7 deletions build.sbt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
val Scala3Version = "3.0.0-RC1"
val Scala3Version = "3.0.0-RC2"
val AkkaVersion = "2.6.13"
val AkkaHttpVersion = "10.2.4"
val circeVersion = "0.14.0-M4"

/**
* [[https://www.scalatest.org/install home page]] and [[https://mvnrepository.com/artifact/org.scalatest/scalatest maven]]
Expand Down Expand Up @@ -31,20 +32,34 @@ lazy val root = project
"com.lightbend.akka" %% "akka-stream-alpakka-file" % "2.0.2",
//http://logback.qos.ch/download.html
"ch.qos.logback" % "logback-classic" % "1.2.3",
"org.typelevel" %% "cats-core" % "2.4.2",
"org.typelevel" %% "cats-free" % "2.4.2",
"net.bblfish.rdf" %% "banana-rdf" % "0.8.5-SNAPSHOT",
"net.bblfish.rdf" %% "banana-jena" % "0.8.5-SNAPSHOT",
"org.tomitribe" % "tomitribe-http-signatures" % "1.7",

//"com.novocode" % "junit-interface" % "0.11" % "test"
).map(_.withDottyCompat(scalaVersion.value)),

// https://circe.github.io/circe/
// libraryDependencies ++= Seq(
// "io.circe" %% "circe-core",
// "io.circe" %% "circe-generic",
// "io.circe" %% "circe-parser"
// ).map(_ % circeVersion),

//https://github.com/filip26/titanium-json-ld
//todo: this should be added to banana-rdf
libraryDependencies ++= Seq(
"com.apicatalog" % "titanium-json-ld" % "1.0.0",
// https://connect2id.com/products/nimbus-jose-jwt/examples/jwk-conversion
"com.nimbusds" % "nimbus-jose-jwt" % "9.7",
"org.glassfish" % "jakarta.json" % "2.0.0"
),

libraryDependencies ++= Seq(
//https://mvnrepository.com/artifact/org.typelevel/cats-core
"org.scalameta" %% "munit" % "0.7.22" % Test,
"org.scalactic" %% "scalactic" % "3.2.5",
"org.scalatest" %% "scalatest" % "3.2.5" % Test
"org.scalameta" %% "munit" % "0.7.23" % Test,
// "org.scalactic" %% "scalactic" % "3.2.5",
"org.scalatest" %% "scalatest" % "3.2.7" % Test,
"org.typelevel" %% "cats-core" % "2.5.0"
),

// java libs
Expand Down
22 changes: 22 additions & 0 deletions src/main/scala/run/cosy/RDF.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package run.cosy

import org.w3.banana.jena.Jena
import akka.http.scaladsl.model.Uri
/**
* Set your preferred implementation of banana-rdf here.
* Note: this way of setting a preferred implementation of RDF means that
* all code referring to this must use one implementation of banana at compile time.
* A more flexible but more tedious approach, where different parts of the code
* could use different RDF implementations, and interact via translations, would require all the
* code to take Rdf and ops implicit parameters `given` with the `using` keyword.
*
**/
object RDF {
export Jena.*
export Jena.given

extension (uri: Uri)
def toRdf = ops.URI(uri.toString)

}

53 changes: 34 additions & 19 deletions src/main/scala/run/cosy/Solid.scala
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import akka.http.scaladsl.Http
import akka.http.scaladsl.Http.ServerBinding
import akka.http.scaladsl.model.Uri.Path.{Empty, Segment, Slash}
import akka.http.scaladsl.model
import akka.http.scaladsl.model.{HttpResponse, Uri}
import akka.http.scaladsl.model.headers
import akka.http.scaladsl.model.{HttpMethods, HttpRequest, HttpResponse, Uri}
import akka.http.scaladsl.server.Directives
import akka.http.scaladsl.server.Directives.{complete, extract, extractRequestContext}
import akka.http.scaladsl.server.{RequestContext, Route, RouteResult}
Expand All @@ -18,9 +19,9 @@ import akka.http.scaladsl.util.FastFuture
import akka.util.Timeout
import cats.data.NonEmptyList
import com.typesafe.config.{Config, ConfigFactory}
import run.cosy.http.RDFMediaTypes
import run.cosy.http.auth.HttpSig
import run.cosy.http.auth.HttpSig.{Agent, PublicKeyAlgo}
import run.cosy.http.{IResponse, RDFMediaTypes, RdfParser}
import run.cosy.http.auth.{HttpSig, SigningData}
import run.cosy.http.auth.HttpSig.{Agent, Anonymous, WebServerAgent}
import run.cosy.ldp.ResourceRegistry
import run.cosy.ldp.fs.BasicContainer

Expand Down Expand Up @@ -59,7 +60,7 @@ object Solid {
.newServerAt(uri.authority.host.address(), uri.authority.port)
.withSettings(serverSettings)
.bind(solid.routeLdp())

ctx.pipeToSelf(serverBinding) {
case Success(binding) =>
val shutdown = CoordinatedShutdown(system)
Expand All @@ -73,11 +74,11 @@ object Solid {
}
shutdown.addTask(CoordinatedShutdown.PhaseServiceStop, "http-shutdown") { () =>
Http().shutdownAllConnectionPools().map(_ => Done)
}
}
Started(binding)
case Failure(ex) => StartFailed(ex)
}

def running(binding: ServerBinding): Behavior[Run] =
Behaviors.receiveMessagePartial[Run] {
case Stop =>
Expand Down Expand Up @@ -116,18 +117,18 @@ object Solid {
case class StartFailed(cause: Throwable) extends Run
case class Started(binding: ServerBinding) extends Run
case object Stop extends Run

}

/**
* The object from from which the solid server is called.
* The object from from which the solid server is called.
* This object also keeps track of actorRef -> path mappings
* so that the requests can go directly to the right actor for a resource
* once all the intermediate containers have been set up.
* once all the intermediate containers have been set up.
*
* We want to have intermediate containers so that some can be setup
* to read from the file system, others from git or CVS, others yet from
* a DB, ... A container actor would know what behavior it implements by
* to read from the file system, others from git or CVS, others yet from
* a DB, ... A container actor would know what behavior it implements by
* looking at some config file in that directory.
*
* This is an object that can be called simultaneously by any number
Expand All @@ -137,9 +138,9 @@ object Solid {
* @param baseUri of the container, eg: https://alice.example/solid/
*/
class Solid(
baseUri: Uri,
path: Path,
registry: ResourceRegistry,
baseUri: Uri,
path: Path,
registry: ResourceRegistry,
rootRef: ActorRef[BasicContainer.Cmd]
)(using sys: ActorSystem[_]) {

Expand All @@ -150,18 +151,32 @@ class Solid(
import scala.jdk.CollectionConverters.*
given timeout: Scheduler = sys.scheduler
given scheduler: Timeout = Timeout(5.second)

def fetch(uri: Uri): Future[PublicKeyAlgo] = ???

def fetchKeyId(keyIdUrl: Uri)(reqc: RequestContext): Future[SigningData] = {
import RouteResult.{Complete,Rejected}
import run.cosy.RDF.{given,_}
given ec: ExecutionContext = reqc.executionContext
val req = RdfParser.rdfRequest(keyIdUrl)
if keyIdUrl.isRelative then //we get the resource locally
routeLdp(WebServerAgent)(reqc.withRequest(req)).flatMap{
case Complete(response) => RdfParser.unmarshalToRDF(response,keyIdUrl).map{ (g: IResponse[Rdf#Graph]) =>
???
}
case Rejected(rejections) => ???
}
else // we get it from the web
???
}

lazy val securedRoute: Route = extractRequestContext { (reqc: RequestContext) =>
HttpSig.httpSignature(reqc)(fetch).optional.tapply {
HttpSig.httpSignature(reqc)(fetchKeyId(_)(reqc)).optional.tapply {
case Tuple1(Some(agent)) => routeLdp(agent)
case Tuple1(None) => routeLdp()
}
}


def routeLdp(agent: Agent = new Agent{}): Route = (reqc: RequestContext) => {
def routeLdp(agent: Agent = new Anonymous()): Route = (reqc: RequestContext) => {
val path = reqc.request.uri.path
import reqc.{given}
reqc.log.info("routing req " + reqc.request.uri)
Expand Down
4 changes: 4 additions & 0 deletions src/main/scala/run/cosy/http/RDFMediaTypes.scala
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,10 @@ object RDFMediaTypes {
fileExtensions = List("srx")
)

def rdfData: Seq[MediaType] = Seq(`application/rdf+xml`, `application/n-triples`, `application/n-quads`,
`text/n-quads`,`text/turtle`,`application/trig`, `text/n3`,`application/ld+json`
)

def all: Seq[MediaType] = Seq(`application/rdf+xml`, `application/n-triples`, `application/n-quads`,
`text/n-quads`,`text/turtle`,`application/trig`, `text/n3`,`application/ld+json`,
`application/sparql-results+json`, `application/sparql-results+xml`, `application/trix`
Expand Down
61 changes: 61 additions & 0 deletions src/main/scala/run/cosy/http/RdfParser.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package run.cosy.http

import akka.http.scaladsl.unmarshalling.{FromEntityUnmarshaller, PredefinedFromEntityUnmarshallers}
import akka.http.scaladsl.model.{ContentType, HttpHeader, HttpRequest, HttpResponse, StatusCode, Uri}

import scala.concurrent.{ExecutionContext, Future}
import org.w3.banana._
import org.w3.banana.syntax._
import org.w3.banana.jena.Jena
import Jena._
import Jena.ops._
import akka.http.scaladsl.util.FastFuture
import akka.stream.Materializer
import org.apache.jena.graph.Graph
import org.w3.banana.io.RDFReader

import scala.util.{Failure, Try}
import scala.util.control.NoStackTrace

object RdfParser {
import RDFMediaTypes.*

/** @param base: the URI at which the document was resolved */
def rdfUnmarshaller(base: Uri): FromEntityUnmarshaller[Rdf#Graph] =
//todo: this loads everything into a string - bad
PredefinedFromEntityUnmarshallers.stringUnmarshaller flatMapWithInput { (entity, string)
def parseWith[T](reader: RDFReader[Rdf,Try,T]) = Future.fromTry {
reader.read(new java.io.StringReader(string), base.toString)
}
//todo: use non blocking parsers
entity.contentType.mediaType match
case `text/turtle` => parseWith(turtleReader)
case `application/rdf+xml` => parseWith(rdfXMLReader)
case `application/n-triples` => parseWith(ntriplesReader)
case `application/ld+json` => parseWith(jsonldReader)
// case `text/html` => new SesameRDFaReader()
case _ => FastFuture.failed(MissingParserException(string.take(400)))
}

def rdfRequest(uri: Uri): HttpRequest =
import akka.http.scaladsl.model.headers.Accept
HttpRequest(uri=uri.withoutFragment)
.addHeader(Accept(`text/turtle`,`application/rdf+xml`,
`application/n-triples`,
`application/ld+json`.withQValue(0.8)//todo: need to update parser in banana
//`text/html`.withQValue(0.2)
)) //we can't specify that we want RDFa in our markup

def unmarshalToRDF(
resp: HttpResponse, base: Uri
)(using ExecutionContext, Materializer): Future[IResponse[Rdf#Graph]] =
import resp._
given FromEntityUnmarshaller[Rdf#Graph] = RdfParser.rdfUnmarshaller(base)
import akka.http.scaladsl.unmarshalling.{Unmarshal, Unmarshaller}
Unmarshal(entity).to[Rdf#Graph].map { g =>
IResponse[Rdf#Graph](base, status, headers, entity.contentType, g)
}


}

108 changes: 108 additions & 0 deletions src/main/scala/run/cosy/http/Web.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package run.cosy.http

import Web.PGWeb
import akka.actor.typed.ActorSystem
import akka.actor.typed.scaladsl.Behaviors
import akka.http.scaladsl.Http
import akka.http.scaladsl.model
import akka.http.scaladsl.unmarshalling.FromEntityUnmarshaller
import model.{ContentType, HttpHeader, HttpRequest, HttpResponse, StatusCode, Uri}
import org.apache.jena.graph.Graph
import org.w3.banana.PointedGraph
import run.cosy.RDF.{given, _}
import akka.stream.Materializer

import scala.concurrent.{ExecutionContext, Future}
import scala.util.{Failure, Success, Try}
import scala.util.control.NonFatal

object Web {
type PGWeb = IResponse[PointedGraph[Rdf]]

extension (uri: Uri)
def toRdf: Rdf#URI = ops.URI(uri.toString)

}

// Need to generalise this, so that it can fetch locally and from the web
class Web(using val ec: ExecutionContext, val as: ActorSystem[Nothing]) {

def GETRdfDoc(uri: Uri, maxRedirect: Int = 4): Future[HttpResponse] =
GET(RdfParser.rdfRequest(uri), maxRedirect).map(_._1)

//todo: add something to the response re number of redirects
//see: https://github.com/akka/akka-http/issues/195
def GET(req: HttpRequest, maxRedirect: Int = 4,
history: List[ResponseSummary] = List()
// keyChain: List[Sig.Client]=List()
): Future[(HttpResponse, List[ResponseSummary])] =

try {
import model.StatusCodes.{Success, Redirection}
Http().singleRequest(req)
.recoverWith { case e => Future.failed(ConnectionException(req.uri.toString, e)) }
.flatMap { resp =>
def summary = ResponseSummary(req.uri, resp.status, resp.headers, resp.entity.contentType)

resp.status match {
case Success(_) => Future.successful((resp, summary :: history))
case Redirection(_) => {
resp.header[model.headers.Location].map { loc =>
val newReq = req.withUri(loc.uri)
resp.discardEntityBytes()
if (maxRedirect > 0)
GET(newReq, maxRedirect - 1, summary :: history)
else Http().singleRequest(newReq).map((_, summary :: history))
}.getOrElse(Future.failed(HTTPException(summary, s"Location header not found on ${resp.status} for ${req.uri}")))
}
//todo later: deal with authorization on remote resources
// case Unauthorized => {
// import akka.http.scaladsl.model.headers.{`WWW-Authenticate`,Date}
// val date = Date(akka.http.scaladsl.model.DateTime.now)
// val reqWithDate = req.addHeader(date)
// val tryFuture = for {
// wwa <- resp.header[`WWW-Authenticate`]
// .fold[Try[`WWW-Authenticate`]](
// Failure(HTTPException(summary,"no WWW-Authenticate header"))
// )(scala.util.Success(_))
// headers <- Try { Sig.Client.signatureHeaders(wwa).get } //<- this should always succeed
// client <- keyChain.headOption.fold[Try[Sig.Client]](
// Failure(AuthException(summary,"no client keys"))
// )(scala.util.Success(_))
// authorization <- client.authorize(reqWithDate,headers)
// } yield {
// GET(reqWithDate.addHeader(authorization), maxRedirect, summary::history, keyChain.tail)
// }
// Future.fromTry(tryFuture).flatten
// }
case _ => {
resp.discardEntityBytes()
Future.failed(StatusCodeException(summary))
}
}
}
} catch {
case NonFatal(e) => Future.failed(ConnectionException(req.uri.toString, e))
}
end GET


def GETrdf(uri: Uri): Future[IResponse[Rdf#Graph]] =
GETRdfDoc(uri).flatMap(RdfParser.unmarshalToRDF(_,uri))


def pointedGET(uri: Uri): Future[PGWeb] = {
import Web.*
GETrdf(uri).map(_.map(PointedGraph[Rdf](uri.toRdf,_)))
}
}

/**
* Interpreted HttpResponse, i.e. the interpretation of a representation
*/
case class IResponse[C](origin: Uri, status: StatusCode,
headers: Seq[HttpHeader], fromContentType: ContentType,
content: C) {
def map[D](f: C => D) = this.copy(content=f(content))
}

Loading

0 comments on commit eca25e0

Please sign in to comment.