Creating a much more user friendly IDP. Required a few things: that we be able to get a certificate without forcing a fetch of it - on entry to the site for example.
--- a/src/main/resources/template/NoWebId.xhtml Sun Dec 11 20:48:10 2011 +0100
+++ b/src/main/resources/template/NoWebId.xhtml Sun Dec 18 14:57:55 2011 +0100
@@ -3,22 +3,6 @@
~ under the MIT licence defined at
~ http://www.opensource.org/licenses/mit-license.html
~
- ~ Permission is hereby granted, free of charge, to any person obtaining a copy of
- ~ this software and associated documentation files (the "Software"), to deal in the
- ~ Software without restriction, including without limitation the rights to use, copy,
- ~ modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
- ~ and to permit persons to whom the Software is furnished to do so, subject to the
- ~ following conditions:
- ~
- ~ The above copyright notice and this permission notice shall be included in all
- ~ copies or substantial portions of the Software.
- ~
- ~ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
- ~ INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
- ~ PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
- ~ HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
- ~ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
- ~ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-->
<html xmlns="http://www.w3.org/1999/xhtml">
--- a/src/main/scala/auth/WebIDSrvc.scala Sun Dec 11 20:48:10 2011 +0100
+++ b/src/main/scala/auth/WebIDSrvc.scala Sun Dec 18 14:57:55 2011 +0100
@@ -25,20 +25,29 @@
import java.io.File
import unfiltered.Cycle
-import xml.{Elem, XML}
-import unfiltered.response.{Html, Ok}
-import org.fusesource.scalate.scuery.Transformer
-import unfiltered.request.{Params, Path}
-import unfiltered.request.&
import java.net.{URLEncoder, URL}
import java.util.Calendar
import org.apache.commons.codec.binary.Base64
-import org.w3.readwriteweb.auth.{WebID, X509CertSigner, X509Claim}
import java.text.SimpleDateFormat
import org.w3.readwriteweb.util.trySome
import unfiltered.request.Params.ParamMapper
-import com.hp.hpl.jena.sparql.vocabulary.FOAF
-import com.hp.hpl.jena.rdf.model.{RDFNode, ModelFactory}
+import com.hp.hpl.jena.rdf.model.ModelFactory
+import sommer.{CertAgent, Extractors}
+import org.w3.readwriteweb.util._
+import org.w3.readwriteweb.auth._
+import unfiltered.request._
+import org.fusesource.scalate.scuery.{Transform, Transformer}
+import org.w3.readwriteweb.netty.ReadWriteWebNetty.StaticFiles
+import java.lang.String
+import xml._
+import unfiltered.response._
+
+object WebIDSrvc {
+ val dateFormat: SimpleDateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ")
+
+ val noImage = "idp/profile_anonymous.png" //"http://eagereyes.org/media/2010/empty-frame.jpg"
+
+}
/**
* @author Henry Story
@@ -48,74 +57,147 @@
implicit def manif: Manifest[Req]
val signer: X509CertSigner
-
- private val dateFormat: SimpleDateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ")
+ import WebIDSrvc._
- /**
- * @param webid a list of webIds identifying the user (only the first few will be used)
- * @param replyTo the service that the response is sent to
- * @return the URL of the response with the webid, timestamp appended and signed
- */
- private def createSignedResponse(webid: Seq[WebID], replyTo: URL): URL = {
- var uri = replyTo.toExternalForm+"?"+ webid.slice(0,3).foldRight("") {
- (wid,str) => "webid="+ URLEncoder.encode(wid.url.toExternalForm, "UTF-8")+"&"
- }
- uri = uri + "ts=" + URLEncoder.encode(dateFormat.format(Calendar.getInstance.getTime), "UTF-8")
- val signedUri = uri +"&sig=" + URLEncoder.encode(new String(Base64.encodeBase64URLSafeString(signer.sign(uri))), "UTF-8")
- return new URL(signedUri)
+
+
+ def sign(urlStr: String): URL = {
+ val timeStampedUrlStr = urlStr + "ts=" + URLEncoder.encode(dateFormat.format(Calendar.getInstance.getTime), "UTF-8")
+ val signedUri = timeStampedUrlStr +
+ "&sig=" + URLEncoder.encode(new String(Base64.encodeBase64URLSafeString(signer.sign(timeStampedUrlStr))), "UTF-8")
+ return new URL(signedUri)
}
- val fileDir: File = new File(this.getClass.getResource("/template/").toURI)
- lazy val webidSrvcPg: Elem = XML.loadFile(new File(fileDir, "WebIdService.login.xhtml"))
- lazy val aboutPg: Elem = XML.loadFile(new File(fileDir, "WebIdService.about.xhtml"))
+ val fileDir: File = new File(this.getClass.getResource("/template/webidp/").toURI)
+
+ /**
+ * using three different templates uses up more memory for the moment, and could be more maintenance work
+ * if all three templates require similar changes, but it makes it easier to visualise the result without
+ * needing a web server.
+ */
+ lazy val errorPg: Elem = XML.loadFile(new File(fileDir, "WebIdService.badcert.html"))
+ lazy val authenticatedPg: Elem = XML.loadFile(new File(fileDir, "WebIdService.auth.html"))
+ lazy val aboutPg: Elem = XML.loadFile(new File(fileDir, "WebIdService.about.html"))
+ lazy val profilePg: Elem = XML.loadFile(new File(fileDir, "WebIdService.entry.html"))
def intent : Cycle.Intent[Req,Res] = {
- case req @ Path(path) if path.startsWith("/srv/idp") => req match {
- case Params(RelyingParty(rp)) & X509Claim(claim) => Ok ~> Html( new ServiceTrans(rp,claim).apply(webidSrvcPg) )
- case _ => Ok ~> Html(aboutTransform(aboutPg))
+ case req @ Path(Seg("srv" :: "idp":: file :: Nil)) => srvStaticFiles(file)
+ case req @ Path("/srv/idp") => req match {
+ case Params(RelyingParty(rp)) => req match {
+ // we authenticate the user only if he has agreed to be authenticated on the page, which we know if the
+ // request is a POST
+ case POST(_) & X509Claim(claim: X509Claim) => { //repetition because of intellij scala 0.5.273 bug
+ val pg = if ( claim.verified.size > 0 ) authenticatedPg else errorPg
+ Ok ~> Html5(new ServiceTrans(rp,claim).apply(pg))
+ }
+ // nevertheless the user may have authenticated allready
+ case GET(_) & XClaim(claim: XClaim) => {
+ val pg = claim match {
+ case NoClaim => profilePg
+ case claim: X509Claim => if ( claim.verified.size > 0 ) authenticatedPg else errorPg
+ }
+ Ok ~> Html5(new ServiceTrans(rp,claim).apply(pg))
+ }
+ }
+ case _ => Ok ~> Html5(aboutTransform(aboutPg))
}
}
-
- object aboutTransform extends Transformer
- class ServiceTrans(relyingParty: URL, claim: X509Claim) extends Transformer {
- val union = claim.verified.flatMap(_.getDefiningModel.toOption).fold(ModelFactory.createDefaultModel()){
- (m1,m2)=>m1.add(m2)
+
+ object TransUtils {
+ //taken from http://stackoverflow.com/questions/2569580/how-to-change-attribute-on-scala-xml-element
+ implicit def addGoodCopyToAttribute(attr: Attribute) = new {
+ def goodcopy(key: String = attr.key, value: Any = attr.value): Attribute =
+ Attribute(attr.pre, key, Text(value.toString), attr.next)
}
- $(".user_name").contents =
- if (claim.verified.size==0)
- claim.cert.getSubjectDN.getName //we have something, we can't rely on it, but we can use it
- else {
- val names = union.listObjectsOfProperty(union.createResource(claim.verified.head.url.toExternalForm),FOAF.name)
- if (!names.hasNext) "nonname"
- else {
- val node: RDFNode = names.next()
- if (node.isLiteral) node.asLiteral().getLexicalForm
- else "anonymous"
- }
- }
- $(".mugshot").attribute("src").value =
- if (claim.verified.size==0)
- "http://www.yourbdnews.com/wp-content/uploads/2011/08/anonymous.jpg"
- else {
- val names = union.listObjectsOfProperty(union.createResource(claim.verified.head.url.toExternalForm),FOAF.depiction)
- if (!names.hasNext) "http://www.yourbdnews.com/wp-content/uploads/2011/08/anonymous.jpg"
- else {
- names.next.toString
- }
+ implicit def iterableToMetaData(items: Iterable[MetaData]): MetaData = {
+ items match {
+ case Nil => Null
+ case head :: tail => head.copy(next=iterableToMetaData(tail))
}
-
- $(".rp_name").contents = relyingParty.getHost
- $(".rp_url").attribute("href").value = createSignedResponse(claim.verified,relyingParty).toExternalForm
+ }
}
+ object srvStaticFiles extends StaticFiles {
+ override def toLocal(file: String) = "/template/webidp/idp/"+file
+ }
+
+ object aboutTransform extends Transformer //todo: need to change public keys in template
+
+ class ServiceTrans(relyingParty: URL, claim: XClaim) extends Transformer {
+ $(".webidform") { node =>
+ val elem = node.asInstanceOf[scala.xml.Elem]
+ import TransUtils._
+ val newelem = elem.copy(attributes=for(attr <- elem.attributes) yield attr match {
+ case attr@Attribute("action", _, _) => attr.goodcopy(value="/srv/idp?rs="+relyingParty.toExternalForm)
+ case other => other
+ })
+ new Transform(newelem) {
+ $(".authenticated") { node =>
+ claim match {
+ case NoClaim => <span/>
+ case _ => new Transform(node) {
+ val union = claim.verified.flatMap(_.getDefiningModel.toOption).fold(ModelFactory.createDefaultModel()) {
+ (m1, m2) => m1.add(m2)
+ }
+ //this works because we have verified before
+ val person = if (union.size() > 0) Extractors.namedPerson(union, claim.verified.head.url)
+ else new CertAgent(claim.cert.getSubjectDN.getName)
+ $(".user_name").contents = person.name
+ $(".mugshot").attribute("src").value = person.depictions.collectFirst {
+ case o => o.toString
+ }.getOrElse(noImage)
+ }.toNodes()
+ }
+ }
+ $(".error").contents = {
+ if (claim==NoClaim) "We received no Certificate"
+ else if (claim.claims.size==0) "Certificate contained no WebID"
+ else if (claim.verified.size==0) "Could not verify any of the WebIDs"
+ else "Some error occured"
+ }
+ $(".response") { nodeRes =>
+ val elemRes = nodeRes.asInstanceOf[scala.xml.Elem]
+ val newElem = elemRes.copy(attributes=for(attr <- elemRes.attributes) yield attr match {
+ case attr@Attribute("href", _, _) => attr.goodcopy(value= {
+ val answer = if (claim == NoClaim) "error=nocert"
+ else if (claim.claims.size == 0) "error=noWebID"
+ else if (claim.verified.size == 0) "error=noVerifiedWebID" +
+ claim.claims.map(claim => claim.verify.failMap(e => e.getMessage)).mkString("&cause=")
+ else claim.verified.slice(0, 3).foldRight("") {
+ (wid, str) => "webid=" + URLEncoder.encode(wid.url.toExternalForm, "UTF-8") + "&"
+ }
+ sign(relyingParty.toExternalForm + "?" + answer).toExternalForm
+ })
+ case other => other
+ })
+ new Transform(newElem) {
+ $(".sitename").contents = relyingParty.getHost
+ }.toNodes()
+ }
+
+ }.toNodes()
+ }
+ }
}
-/**
+case class Html5(nodes: scala.xml.NodeSeq) extends ComposeResponse(HtmlContent ~> {
+ val w = new java.io.StringWriter()
+ val html = nodes.head match {
+ case <html>{_*}</html> => nodes.head
+ case _ => <html>{nodes}</html>
+ }
+ xml.XML.write( w, html, "UTF-8", xmlDecl = false, doctype =
+ xml.dtd.DocType( "html", xml.dtd.SystemID( "about:legacy-compat" ), Nil ))
+ ResponseString(w.toString)
+})
+
+
+ /**
* similar to Extract superclass, but is useful when one has a number of attributes that
* have the same meaning. This can arise if one has legacy URLS or if one has code that
* has human readable and shorter ones
@@ -128,5 +210,4 @@
object urlMap extends ParamMapper(_.flatMap(u=>trySome{new URL(u)}).headOption)
object RelyingParty extends ExtractN(List("rs","authreqissuer"), urlMap )
-
-
+object Login extends Params.Extract("login",_=>Some(true))
--- a/src/main/scala/auth/WebIdClaim.scala Sun Dec 11 20:48:10 2011 +0100
+++ b/src/main/scala/auth/WebIdClaim.scala Sun Dec 18 14:57:55 2011 +0100
@@ -35,8 +35,7 @@
/**
* @author Henry Story
- * @created: 13/10/2011
- */
+ **/
/**
* One can only construct a WebID via the WebIDClaim apply
--- a/src/main/scala/auth/X509Cert.scala Sun Dec 11 20:48:10 2011 +0100
+++ b/src/main/scala/auth/X509Cert.scala Sun Dec 18 14:57:55 2011 +0100
@@ -192,11 +192,11 @@
object Certs {
- def unapplySeq[T](r: HttpRequest[T])(implicit m: Manifest[T]): Option[IndexedSeq[Certificate]] = {
+ def unapplySeq[T](r: HttpRequest[T])(implicit m: Manifest[T], fetch: Boolean=true): Option[IndexedSeq[Certificate]] = {
if (m <:< manifest[HttpServletRequest])
unapplyServletRequest(r.asInstanceOf[HttpRequest[HttpServletRequest]])
else if (m <:< manifest[ReceivedMessage])
- unapplyReceivedMessage(r.asInstanceOf[HttpRequest[ReceivedMessage]])
+ unapplyReceivedMessage(r.asInstanceOf[HttpRequest[ReceivedMessage]],fetch)
else
None //todo: should throw an exception here?
}
@@ -210,24 +210,27 @@
case _ => None
}
- private def unapplyReceivedMessage[T <: ReceivedMessage](r: HttpRequest[T]): Option[IndexedSeq[Certificate]] = {
+ private def unapplyReceivedMessage[T <: ReceivedMessage](r: HttpRequest[T], fetch: Boolean): Option[IndexedSeq[Certificate]] = {
import org.jboss.netty.handler.ssl.SslHandler
val sslh = r.underlying.context.getPipeline.get(classOf[SslHandler])
trySome(sslh.getEngine.getSession.getPeerCertificates.toIndexedSeq) orElse {
- sslh.setEnableRenegotiation(true)
- r match {
- case UserAgent(agent) if needAuth(agent) => sslh.getEngine.setNeedClientAuth(true)
- case _ => sslh.getEngine.setWantClientAuth(true)
+ if (!fetch) None
+ else {
+ sslh.setEnableRenegotiation(true)
+ r match {
+ case UserAgent(agent) if needAuth(agent) => sslh.getEngine.setNeedClientAuth(true)
+ case _ => sslh.getEngine.setWantClientAuth(true)
+ }
+ val future = sslh.handshake()
+ future.await(30000) //that's certainly way too long.
+ if (future.isDone && future.isSuccess)
+ trySome(sslh.getEngine.getSession.getPeerCertificates.toIndexedSeq)
+ else
+ None
}
- val future = sslh.handshake()
- future.await(30000) //that's certainly way too long.
- if (future.isDone && future.isSuccess)
- trySome(sslh.getEngine.getSession.getPeerCertificates.toIndexedSeq)
- else
- None
}
}
--- a/src/main/scala/auth/X509Claim.scala Sun Dec 11 20:48:10 2011 +0100
+++ b/src/main/scala/auth/X509Claim.scala Sun Dec 18 14:57:55 2011 +0100
@@ -27,20 +27,15 @@
import org.slf4j.LoggerFactory
import java.security.cert.X509Certificate
-import org.w3.readwriteweb.WebCache
import javax.security.auth.Refreshable
-import java.util.Date
import collection.JavaConversions._
import unfiltered.request.HttpRequest
import java.security.interfaces.RSAPublicKey
import collection.immutable.List
-import collection.mutable.HashMap
-import scalaz.{Scalaz, Success, Validation}
-import Scalaz._
-import java.security.PublicKey
import com.google.common.cache.{CacheLoader, CacheBuilder, Cache}
import java.util.concurrent.TimeUnit
import org.w3.readwriteweb.util.trySome
+import java.util.Date
/**
* @author hjs
@@ -49,7 +44,7 @@
object X509Claim {
final val logger = LoggerFactory.getLogger(classOf[X509Claim])
-
+ implicit val fetch = true //fetch the certificate if we don't have it
// this is cool because it is not in danger of running out of memory but it makes it impossible to create the claim
// with an implicit WebCache...
@@ -66,7 +61,6 @@
}
-
/**
* Extracts the URIs in the subject alternative name extension of an X.509
* certificate
@@ -80,13 +74,42 @@
case coll if (coll != null) => {
for {
sanPair <- coll if (sanPair.get(0) == 6)
- } yield sanPair(1).asInstanceOf[String]
+ } yield sanPair(1).asInstanceOf[String].trim
}
case _ => Nil
}
}
+object ExistingClaim {
+ implicit val fetch = false //don't fetch the certificate if we don't have it -- ie, don't force the fetching of it
+
+ def unapply[T](r: HttpRequest[T])(implicit m: Manifest[T]): Option[X509Claim] = r match {
+ case Certs(c1: X509Certificate, _*) => trySome(X509Claim.idCache.get(c1))
+ case _ => None
+ }
+
+}
+
+object XClaim {
+ implicit val fetch = false
+ def unapply[T](r: HttpRequest[T])(implicit m: Manifest[T]): Option[XClaim] = r match {
+ case Certs(c1: X509Certificate, _*) => trySome(X509Claim.idCache.get(c1))
+ case _ => Some(NoClaim)
+ }
+}
+
+
+
+/**
+ * This looks like something that could be abstracted into type perhaps XClaim[X509Certificate,WebIDClaim]
+ */
+sealed trait XClaim {
+ def cert: X509Certificate
+ def valid: Boolean
+ def claims: List[WebIDClaim]
+ def verified: List[WebID]
+}
/**
* An X509 Claim maintains information about the proofs associated with claims
@@ -99,7 +122,7 @@
* @created: 30/03/2011
*/
// can't be a case class as it then creates object which clashes with defined one
-class X509Claim(val cert: X509Certificate) extends Refreshable {
+class X509Claim(val cert: X509Certificate) extends Refreshable with XClaim {
import X509Claim._
val claimReceivedDate = new Date()
@@ -107,9 +130,11 @@
lazy val tooEarly = claimReceivedDate.before(cert.getNotBefore())
- lazy val webidclaims: List[WebIDClaim] = getClaimedWebIds(cert) map { webid => new WebIDClaim(webid, cert.getPublicKey.asInstanceOf[RSAPublicKey]) }
+ lazy val claims: List[WebIDClaim] = getClaimedWebIds(cert) map { webid =>
+ new WebIDClaim(webid, cert.getPublicKey.asInstanceOf[RSAPublicKey])
+ }
- lazy val verified: List[WebID] = webidclaims.flatMap(_.verify.toOption)
+ lazy val verified: List[WebID] = claims.flatMap(_.verify.toOption)
//note could also implement Destroyable
//
@@ -125,7 +150,7 @@
/* The certificate is currently within the valid time zone */
override def isCurrent(): Boolean = ! (tooLate || tooEarly)
- def canEqual(other: Any) = other.isInstanceOf[X509Claim]
+ protected def canEqual(other: Any) = other.isInstanceOf[X509Claim]
override def equals(other: Any): Boolean = other match {
case that: X509Claim => (that eq this) || (that.canEqual(this) && cert == that.cert)
@@ -135,5 +160,18 @@
override lazy val hashCode: Int =
41 * (41 + (if (cert != null) cert.hashCode else 0))
+ def valid = isCurrent()
}
+/**
+ * A bit like a None for X509Claims
+ */
+case object NoClaim extends XClaim {
+ override def cert = throw new NoSuchElementException("None.get")
+
+ def valid = false
+
+ def claims = List.empty
+
+ def verified = List.empty
+}
\ No newline at end of file
--- a/src/main/scala/auth/X509view.scala Sun Dec 11 20:48:10 2011 +0100
+++ b/src/main/scala/auth/X509view.scala Sun Dec 18 14:57:55 2011 +0100
@@ -89,7 +89,7 @@
}
$(".no_webid") { node => if (x509.verified.size==0) node else <span/> }
$(".webid_test") { node =>
- val ff = for (idclaim <- x509.webidclaims) yield {
+ val ff = for (idclaim <- x509.claims) yield {
val idAsrt = new Assertion(webidClaimTst, idclaim)
new Transform(node) {
$(".webid").contents = idclaim.san
--- a/src/main/scala/auth/earl.scala Sun Dec 11 20:48:10 2011 +0100
+++ b/src/main/scala/auth/earl.scala Sun Dec 18 14:57:55 2011 +0100
@@ -123,8 +123,8 @@
}
object certProvidedSan extends TestObj[X509Claim]("certificateProvidedSAN") {
- def apply(x509: X509Claim) = new Result(" There are "+x509.webidclaims.size+" SANs in the certificate",
- x509.webidclaims.size >0)
+ def apply(x509: X509Claim) = new Result(" There are "+x509.claims.size+" SANs in the certificate",
+ x509.claims.size >0)
}
--- a/src/main/scala/netty/ReadWriteWebNetty.scala Sun Dec 11 20:48:10 2011 +0100
+++ b/src/main/scala/netty/ReadWriteWebNetty.scala Sun Dec 18 14:57:55 2011 +0100
@@ -29,10 +29,12 @@
import org.w3.readwriteweb.auth.{X509view, RDFAuthZ}
import org.w3.readwriteweb._
import org.jboss.netty.handler.codec.http.HttpResponse
-import unfiltered.netty.{ServerErrorResponse, ReceivedMessage, cycle}
import unfiltered.request.Path
-import unfiltered.netty.async
-import unfiltered.response.{JsContent, NotFound, ResponseString, Ok}
+import java.io.{InputStream, OutputStream}
+import unfiltered.response._
+import unfiltered.netty._
+import collection.immutable.List
+import java.lang.String
/**
* ReadWrite Web for Netty server, allowing TLS renegotiation
@@ -78,19 +80,49 @@
}
- object publicStatic extends cycle.Plan with cycle.ThreadPool with ServerErrorResponse {
+
+ trait StaticFiles extends PartialFunction[String, ResponseFunction[Any]] {
+ /* override this if the local path is somehow different from the url path */
+ def toLocal(webpath: String): String = webpath
+ val extension = "([^\\.]*?)$".r
+ val extList: List[String] = List("css", "png")
+
+ private def toString(in: InputStream): String = {
+ val source = scala.io.Source.fromInputStream(in)
+ val lines = source.mkString
+ source.close()
+ lines
+ }
+
+ def isDefinedAt(path: String): Boolean = try {
+ val in = classOf[StaticFiles].getResourceAsStream(toLocal(path))
+ (in != null) & (extension.findFirstIn(path).exists(extList contains _))
+ } catch {
+ case _ => false
+ }
+
+ def apply(path: String): ResponseFunction[Any] = {
+ try {
+ val in = classOf[StaticFiles].getResourceAsStream(toLocal(path))
+ extension.findFirstIn(path).getOrElse("css") match {
+ case "css" => Ok ~> ResponseString(toString(in)) ~> CssContent
+ case "js" => Ok ~> ResponseString(toString(in)) ~> JsContent
+ case "png" => Ok ~> ResponseBin(in) ~> ContentType("image/png")
+ }
+ } catch {
+ case _ => NotFound
+ }
+
+ }
+
+
+ }
+
+ object publicStatic extends cycle.Plan with cycle.ThreadPool with ServerErrorResponse with StaticFiles {
+ val initialPath= "/public"
+
def intent = {
- case Path(path) if path.startsWith("/public") => {
- try {
- val in = publicStatic.getClass.getResourceAsStream(path)
- val source = scala.io.Source.fromInputStream(in)
- val lines = source.mkString
- source.close()
- Ok ~> ResponseString(lines) ~> JsContent //currently that's all I am interested in, but of course this is not right.
- } catch {
- case _ => NotFound
- }
- }
+ case Path(path) if path.startsWith(initialPath) => apply(path)
}
}
@@ -105,3 +137,14 @@
}
+
+case class ResponseBin(bis: InputStream) extends ResponseStreamer {
+ override def stream(out: OutputStream) {
+ var c=0
+ val buf = new Array[Byte](1024)
+ do {
+ c = bis.read(buf)
+ if (c > 0) out.write(buf,0,c)
+ } while (c > -1)
+ }
+}
\ No newline at end of file
--- a/src/main/scala/sommer/GraphReader.scala Sun Dec 11 20:48:10 2011 +0100
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,185 +0,0 @@
-/*
- * Copyright (c) 2011 Henry Story (bblfish.net)
- * under the MIT licence defined at
- * http://www.opensource.org/licenses/mit-license.html
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy of
- * this software and associated documentation files (the "Software"), to deal in the
- * Software without restriction, including without limitation the rights to use, copy,
- * modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
- * and to permit persons to whom the Software is furnished to do so, subject to the
- * following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
- * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
- * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
- * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
- * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
- * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
- */
-
-package sommer
-
-import com.hp.hpl.jena.vocabulary.RDF
-import com.hp.hpl.jena.sparql.vocabulary.FOAF
-import java.lang.String
-import org.w3.readwriteweb.{Resource, WebCache}
-import scalaz.Validation
-import java.net.{URI, URL}
-import collection._
-import com.hp.hpl.jena.rdf.model.{RDFNode, Literal, ResourceFactory, ModelFactory, Resource => JResource, Model}
-import collection.JavaConverters._
-
-
-/**
- * Some initial ideas on mappers between rdf and scala classes
- * exploring ideas from
- * http://dl.dropbox.com/u/7810909/docs/reader-monad/chunk-html/index.html
- *
- * @author Henry Story
- */
-
-case class GraphReader[A](extract: Resource => Validation[scala.Throwable,A]) {
- def map[B](g: A => B) = GraphReader(r => extract(r).map(g(_)))
- def flatMap[B](g: A => GraphReader[B]): GraphReader[B] = GraphReader(r => extract(r).map(g(_)).flatMap(_.extract(r)))
-}
-
-case class Person(name: String)
-
-case class IdPerson(webid: JResource) {
-
- // this map is no longer necessary if we use the graph mapped resource
- val relations : mutable.MultiMap[JResource,RDFNode] = new mutable.HashMap[JResource, mutable.Set[RDFNode]] with mutable.MultiMap[JResource,RDFNode]
- cacheProp
-
- import Extractors.toProperty
- // a couple of useful methods for foaf relations. One could add a few others for other vocabs
- def foafRel(p: String) = relations.get(toProperty(FOAF.NS+p))
-
- //very very simple implementation, not taking account of first/last name, languages etc...
- def name = {
- foafRel("name").mkString(" ")
- }
-
- /**
- * Notice how here the graph has been mapped into the object, via the webid JResource that still
- * has a pointer to the graph.
- */
- def cacheProp {
- for (s <- webid.listProperties().asScala
- if (!s.getObject.isAnon)
- ) {
- relations.addBinding(s.getPredicate, s.getObject)
- }
- }
-
-}
-
-
-object Extractors {
- type Val[A] = Validation[scala.Throwable,A]
-
- def findPeople(m: Resource): Validation[scala.Throwable,Set[Person]] = {
- for (gr<-m.get) yield {
- for (st <- gr.listStatements(null,RDF.`type`,FOAF.Person).asScala;
- val subj = st.getSubject;
- st2 <- gr.listStatements(subj, FOAF.name,null).asScala
- ) yield {
- new Person(st2.getObject.asLiteral().toString)
- }
- }.toSet
- }
-
- def findDefinedPeople(m: Resource): Validation[scala.Throwable,Set[IdPerson]] = {
- for (gr<-m.get) yield {
- for (st <- gr.listStatements(null,RDF.`type`,FOAF.Person).asScala;
- val subj = st.getSubject;
- if (subj.isURIResource && subj.toString.startsWith(m.name.toString));
- st2 <- gr.listStatements(subj, FOAF.name,null).asScala
- ) yield {
- new IdPerson(subj)
- }
- }.toSet
- }
-
- def findIdPeople(m: Resource): Val[Set[IdPerson]] = {
- for (gr<-m.get) yield {
- for (st <- gr.listStatements(null,RDF.`type`,FOAF.Person).asScala;
- val subj = st.getSubject;
- if (subj.isURIResource)
- ) yield {
- val p = new IdPerson(subj)
-
- p
- }
- }.toSet
-
- }
-
- implicit def toResource(str: String) = ResourceFactory.createResource(str)
- implicit def toProperty(str: String) = ResourceFactory.createProperty(str)
-
-}
-
-object Test {
- implicit def urlToResource(u: URL) = WebCache.resource(u)
- import System._
-
- val peopleRd = new GraphReader[Set[Person]](Extractors.findPeople)
- val definedPeopleRd = new GraphReader[Set[IdPerson]](Extractors.findDefinedPeople)
- val idPeopleRd = new GraphReader[Set[IdPerson]](Extractors.findIdPeople)
- val definedPeopleFriends = definedPeopleRd.flatMap(people =>GraphReader[Set[IdPerson]]{
- resource: Resource =>
- resource.get.map(gr=>
- for ( p <- people;
- st <- gr.listStatements(p.webid, FOAF.knows, null).asScala ;
- val friend = st.getObject;
- if (friend.isURIResource)
- ) yield IdPerson(friend.asInstanceOf[JResource])
- )
- } )
-
- def main(args: Array[String]) {
-
- val url: String = "http://bblfish.net/people/henry/card"
-
- // extract the people who are defined in the graph (rarely more than one)
- for (people <- definedPeopleRd.extract(new URL(url));
- p <- people) {
- System.out.println("found "+p.name)
- }
- out.println
-
- out.println("friends of people defined using flatmaped reader")
- //use the flatMap to go from defined people to their friends
- //get these friends names
- for (people <- definedPeopleFriends.extract(new URL(url));
- p <- people) {
- System.out.println("found "+p.name)
- }
- out.println
-
-
-
- // extract all the people with ids.
- // and show all their properties
- // this produces a lot of data so its commented out
-// out.println("=== ID PEOPLE ===")
-// out.println
-// for (people <- idPeopleRd.extract(new URL(url));
-// p <- people) {
-// out.println
-// out.println("----------")
-// out.println("id "+p.webid)
-// out.println("with properties:")
-// val str = for ((prop,setovals) <- p.relations.iterator) yield {
-// setovals.map(n=>prop.getLocalName+" is "+n.toString).mkString("\n")
-// }
-// out.print(str.mkString("\r\n"))
-// }
-
- }
-}
\ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/scala/sommer/ResourceReader.scala Sun Dec 18 14:57:55 2011 +0100
@@ -0,0 +1,222 @@
+/*
+ * Copyright (c) 2011 Henry Story (bblfish.net)
+ * under the MIT licence defined at
+ * http://www.opensource.org/licenses/mit-license.html
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of
+ * this software and associated documentation files (the "Software"), to deal in the
+ * Software without restriction, including without limitation the rights to use, copy,
+ * modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
+ * and to permit persons to whom the Software is furnished to do so, subject to the
+ * following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+ * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
+ * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+ * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+package sommer
+
+import com.hp.hpl.jena.vocabulary.RDF
+import com.hp.hpl.jena.sparql.vocabulary.FOAF
+import java.lang.String
+import org.w3.readwriteweb.{Resource, WebCache}
+import scalaz.Validation
+import java.net.URL
+import collection._
+import collection.JavaConverters._
+import com.hp.hpl.jena.rdf.model.{Model, ResourceFactory, Resource => JResource}
+import java.security.cert.X509Certificate
+
+
+/**
+ * Some initial ideas on mappers between rdf and scala classes
+ * exploring ideas from
+ * http://dl.dropbox.com/u/7810909/docs/reader-monad/chunk-html/index.html
+ *
+ * @author Henry Story
+ */
+
+
+/**
+ * Resource readers are Monads
+ * These function directly on types given by resources specified by URIs
+ * But the map from resource to validation, makes things a bit heavy.
+ */
+case class ResourceReader[A](extract: Resource => Validation[scala.Throwable,A]) {
+ def map[B](g: A => B) = ResourceReader(r => extract(r).map(g(_)))
+ def flatMap[B](g: A => ResourceReader[B]): ResourceReader[B] = ResourceReader(r => extract(r).map(g(_)).flatMap(_.extract(r)))
+}
+
+trait Agent {
+ def name: String
+ def foaf(attr: String): Iterator[AnyRef] = Iterator.empty
+ def depictions: Iterator[JResource] = Iterator.empty
+}
+
+object CertAgent {
+ val CNregex = "cn=(.*?),".r
+}
+
+/**
+ *Information about an agent gleaned from the certificate
+ *(One could generalise this by having a function from certificates to graphs)
+ **/
+class CertAgent(dn : String) extends Agent {
+ val name = CertAgent.CNregex.findFirstMatchIn(dn).map(_.group(1)).getOrElse("(unnamed)")
+
+ override def foaf(attr: String) = if (attr == "name") Iterator(name) else Iterator.empty
+
+}
+
+case class Person(name: String)
+
+
+
+case class IdPerson(id: JResource) extends Agent {
+
+
+ import Extractors.toProperty
+ // a couple of useful methods for foaf relations. One could add a few others for other vocabs
+ override def foaf(attr: String) = id.listProperties(toProperty(FOAF.NS+attr)).asScala.map(_.getObject)
+
+ //very very simple implementation, not taking account of first/last name, languages etc...
+ override def name = {
+ foaf("name").mkString(" ")
+ }
+
+ override def depictions = foaf("depiction").collect{case n if n.isURIResource => n.asResource()}
+
+}
+
+object ANONYMOUS extends Agent {
+ val pix = ResourceFactory.createResource("http://massivnews.com/wp-content/uploads/2011/07/Anonymous-000006.jpg")
+ override val name = "_ANONYMOUS_"
+ override def foaf(attr: String) = if (attr == "name") Iterator(name) else Iterator.empty
+ override val depictions = List(pix).iterator
+}
+
+
+object Extractors {
+ type Val[A] = Validation[scala.Throwable,A]
+
+ def findPeople(m: Resource): Validation[scala.Throwable,Set[Person]] = {
+ for (gr<-m.get) yield {
+ for (st <- gr.listStatements(null,RDF.`type`,FOAF.Person).asScala;
+ val subj = st.getSubject;
+ st2 <- gr.listStatements(subj, FOAF.name,null).asScala
+ ) yield {
+ new Person(st2.getObject.asLiteral().toString)
+ }
+ }.toSet
+ }
+
+ def definedPeople(gr: Model, doc: URL): Iterator[IdPerson] = {
+ for (st <- gr.listStatements(null, RDF.`type`, FOAF.Person).asScala;
+ val subj = st.getSubject;
+ //todo: come up with a better definition of "is defined in"
+ if (subj.isURIResource && subj.toString.split("#")(0) == doc.toString.split("#")(0));
+ st2 <- gr.listStatements(subj, FOAF.name, null).asScala
+ ) yield {
+ new IdPerson(subj)
+ }
+ }
+
+ /**
+ * Argh. Jena does not make a good difference between read only models, and RW ones
+ * So one should verify the person exists before doing this if one does not want to create a RW model
+ */
+ def namedPerson(gr: Model, webid: URL): IdPerson = {
+ IdPerson(gr.createResource(webid.toString))
+ }
+
+ def findDefinedPeople(m: Resource): Validation[scala.Throwable,Set[IdPerson]] = {
+ for (gr<-m.get) yield {
+ definedPeople(gr, m.name)
+ }.toSet
+ }
+
+ def findIdPeople(m: Resource): Val[Set[IdPerson]] = {
+ for (gr<-m.get) yield {
+ for (st <- gr.listStatements(null,RDF.`type`,FOAF.Person).asScala;
+ val subj = st.getSubject;
+ if (subj.isURIResource)
+ ) yield {
+ val p = new IdPerson(subj)
+
+ p
+ }
+ }.toSet
+
+ }
+
+ implicit def toResource(str: String) = ResourceFactory.createResource(str)
+ implicit def toProperty(str: String) = ResourceFactory.createProperty(str)
+
+}
+
+object Test {
+ implicit def urlToResource(u: URL) = WebCache.resource(u)
+ import System._
+
+ val peopleRd = new ResourceReader[Set[Person]](Extractors.findPeople)
+ val definedPeopleRd = new ResourceReader[Set[IdPerson]](Extractors.findDefinedPeople)
+ val idPeopleRd = new ResourceReader[Set[IdPerson]](Extractors.findIdPeople)
+ val definedPeopleFriends = definedPeopleRd.flatMap(people =>ResourceReader[Set[IdPerson]]{
+ resource: Resource =>
+ resource.get.map(gr=>
+ for ( p <- people;
+ st <- gr.listStatements(p.id, FOAF.knows, null).asScala ;
+ val friend = st.getObject;
+ if (friend.isURIResource)
+ ) yield IdPerson(friend.asInstanceOf[JResource])
+ )
+ } )
+
+ def main(args: Array[String]) {
+
+ val url: String = "http://bblfish.net/people/henry/card"
+
+ // extract the people who are defined in the graph (rarely more than one)
+ for (people <- definedPeopleRd.extract(new URL(url));
+ p <- people) {
+ System.out.println("found "+p.name)
+ }
+ out.println
+
+ out.println("friends of people defined using flatmaped reader")
+ //use the flatMap to go from defined people to their friends
+ //get these friends names
+ for (people <- definedPeopleFriends.extract(new URL(url));
+ p <- people) {
+ System.out.println("found "+p.name)
+ }
+ out.println
+
+
+
+ // extract all the people with ids.
+ // and show all their properties
+ // this produces a lot of data so its commented out
+// out.println("=== ID PEOPLE ===")
+// out.println
+// for (people <- idPeopleRd.extract(new URL(url));
+// p <- people) {
+// out.println
+// out.println("----------")
+// out.println("id "+p.webid)
+// out.println("with properties:")
+// val str = for ((prop,setovals) <- p.relations.iterator) yield {
+// setovals.map(n=>prop.getLocalName+" is "+n.toString).mkString("\n")
+// }
+// out.print(str.mkString("\r\n"))
+// }
+
+ }
+}
\ No newline at end of file